From fa4f327a3f2c3231fad2c55674321d870345e1cc Mon Sep 17 00:00:00 2001 From: Marcus van Elst Date: Wed, 6 May 2026 16:05:24 +0200 Subject: [PATCH] Schritt 10: GuiSchedulerTab implementieren und in Workspace verdrahten MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SchedulerControlUseCase um getIntervalSeconds(), saveIntervalSeconds(), disableAutostart() erweitert - DefaultSchedulerControlUseCase implementiert diese drei neuen Methoden - GuiSchedulerTab neu eingeführt: Autostart-Fehler-Banner + Scheduler-Steuerung (Status, Start/Stopp, Countdown, letzter Lauf, Fehleranzeige, Intervall-Feld) - GuiConfigurationEditorWorkspace: schedulerTab als 3. Tab (nach Verarbeitungslauf) eingehängt; onSchedulerStatusRefresh delegiert jetzt auch an schedulerTab.updateStatus() - GuiAdapterSmokeTest: Tab-Anzahl und -Reihenfolge auf 5 Tabs aktualisiert Co-Authored-By: Claude Sonnet 4.6 --- .../gui/GuiConfigurationEditorWorkspace.java | 19 +- .../adapter/in/gui/GuiSchedulerTab.java | 474 ++++++++++++++++++ .../adapter/in/gui/GuiAdapterSmokeTest.java | 14 +- .../port/in/SchedulerControlUseCase.java | 36 ++ .../DefaultSchedulerControlUseCase.java | 36 ++ 5 files changed, 569 insertions(+), 10 deletions(-) create mode 100644 pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiSchedulerTab.java diff --git a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiConfigurationEditorWorkspace.java b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiConfigurationEditorWorkspace.java index 2b916a6..518cc10 100644 --- a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiConfigurationEditorWorkspace.java +++ b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiConfigurationEditorWorkspace.java @@ -459,13 +459,19 @@ public final class GuiConfigurationEditorWorkspace { private final GuiBatchRunTab batchRunTab; /** - * Dritter Haupt-Tab: Historien-Tab „Verlauf". Wird während der Workspace-Konstruktion + * Dritter Haupt-Tab: Scheduler-Steuerung. Wird während der Workspace-Konstruktion + * erstellt und in den {@link #tabPane} eingehängt. + */ + private final GuiSchedulerTab schedulerTab; + + /** + * Vierter Haupt-Tab: Historien-Tab „Verlauf". Wird während der Workspace-Konstruktion * erstellt und in den {@link #tabPane} eingehängt. */ private final de.gecheckt.pdf.umbenenner.adapter.in.gui.history.GuiHistoryTab historyTab; /** - * Vierter Haupt-Tab: Prompt-Editor. Wird während der Workspace-Konstruktion erstellt + * Fünfter Haupt-Tab: Prompt-Editor. Wird während der Workspace-Konstruktion erstellt * und in den {@link #tabPane} eingehängt. */ private final GuiPromptEditorTab promptEditorTab; @@ -557,6 +563,10 @@ public final class GuiConfigurationEditorWorkspace { this::editorTargetFolder, effectiveContext.configurationFileLockPort()); + this.schedulerTab = new GuiSchedulerTab( + effectiveContext.schedulerControlUseCase(), + () -> editorState.isDirty()); + this.historyTab = new de.gecheckt.pdf.umbenenner.adapter.in.gui.history.GuiHistoryTab( effectiveContext.historyOverviewPort(), effectiveContext.historyDetailsPort(), @@ -1091,14 +1101,15 @@ public final class GuiConfigurationEditorWorkspace { * - * Der Scheduler-Tab wird in einem späteren Implementierungsschritt hinzugefügt. * * @param status aktueller Scheduler-Status; darf nicht {@code null} sein */ public void onSchedulerStatusRefresh(SchedulerStatus status) { batchRunTab.updateSchedulerState(status); + schedulerTab.updateStatus(status); updateLockState(status); } @@ -1655,7 +1666,7 @@ public final class GuiConfigurationEditorWorkspace { scrollPane.setPadding(new Insets(0)); editorTab.setContent(scrollPane); - tabPane.getTabs().setAll(editorTab, batchRunTab.tab(), historyTab.tab(), promptEditorTab.tab()); + tabPane.getTabs().setAll(editorTab, batchRunTab.tab(), schedulerTab.tab(), historyTab.tab(), promptEditorTab.tab()); root.setCenter(tabPane); // Tab-Wechsel-Schutz: Beim Wechsel weg vom Verarbeitungslauf-Tab prüfen ob diff --git a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiSchedulerTab.java b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiSchedulerTab.java new file mode 100644 index 0000000..774e542 --- /dev/null +++ b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiSchedulerTab.java @@ -0,0 +1,474 @@ +package de.gecheckt.pdf.umbenenner.adapter.in.gui; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; +import java.util.Optional; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.function.Supplier; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import de.gecheckt.pdf.umbenenner.application.port.in.SchedulerControlUseCase; +import de.gecheckt.pdf.umbenenner.application.port.in.SchedulerStartException; +import de.gecheckt.pdf.umbenenner.application.port.in.SchedulerState; +import de.gecheckt.pdf.umbenenner.application.port.in.SchedulerStatus; +import de.gecheckt.pdf.umbenenner.application.port.out.RunSummary; +import javafx.application.Platform; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.Alert; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.Separator; +import javafx.scene.control.Tab; +import javafx.scene.control.TextField; +import javafx.scene.control.Tooltip; +import javafx.scene.layout.HBox; +import javafx.scene.layout.VBox; + +/** + * Fünfter Haupt-Tab des JavaFX-Editorfensters: die Scheduler-Steuerungsansicht. + *

+ * Zeigt den aktuellen Zustand des automatischen Schedulers und erlaubt dessen + * Steuerung über {@link SchedulerControlUseCase}. Der Tab-Inhalt wird im Sekundentakt + * durch {@link #updateStatus(SchedulerStatus)} aktualisiert, das von der zentralen + * {@link GuiStatusRefreshTimeline} aufgerufen wird. + * + *

Bereiche

+ * + * + *

Threading

+ *

Alle öffentlichen Methoden müssen auf dem JavaFX Application Thread aufgerufen + * werden. Start-, Stopp- und Speichern-Aktionen werden auf einem dedizierten + * Hintergrund-Worker-Thread ({@code gui-scheduler-control}) ausgeführt. + */ +public final class GuiSchedulerTab { + + private static final Logger LOG = LogManager.getLogger(GuiSchedulerTab.class); + + private static final String TAB_TITLE = "Scheduler"; + + /** Mindestwert für das konfigurierbare Ausführungsintervall. */ + static final int MIN_INTERVAL_SECONDS = 30; + + private static final DateTimeFormatter TIME_FORMATTER = + DateTimeFormatter.ofPattern("HH:mm").withZone(ZoneId.systemDefault()); + + private final Tab tab = new Tab(TAB_TITLE); + private final Optional schedulerUseCase; + private final Supplier isConfigDirty; + + // ------------------------------------------------------------------------- + // Bereich 1: Scheduler-Steuerung + // ------------------------------------------------------------------------- + + private final Label statusLabel = new Label("○ Gestoppt"); + private final Button startButton = new Button("Scheduler starten"); + private final Button stopButton = new Button("Scheduler stoppen"); + private final Label nextTickLabel = new Label(); + private final Label lastRunLabel = new Label("Noch kein Lauf in dieser Sitzung."); + private final Label lastErrorLabel = new Label(); + private final TextField intervalField = new TextField(); + private final Label intervalValidationLabel = new Label(); + + // ------------------------------------------------------------------------- + // Bereich 2: Autostart-Fehler-Banner + // ------------------------------------------------------------------------- + + private final VBox autostartErrorBanner = new VBox(6); + private final Label autostartErrorDetailLabel = new Label(); + private final Button autostartStartButton = new Button("Scheduler starten"); + private final Button autostartDisableButton = new Button("Autostart deaktivieren"); + + /** + * Wenn {@code true}, wird das Autostart-Fehler-Banner dauerhaft ausgeblendet + * (weil der Benutzer „Autostart deaktivieren" geklickt hat). + */ + private boolean autostartBannerDismissed = false; + + private final ExecutorService workerExecutor = Executors.newSingleThreadExecutor(r -> { + Thread t = new Thread(r, "gui-scheduler-control"); + t.setDaemon(true); + return t; + }); + + /** + * Erstellt einen neuen Scheduler-Tab. + * + * @param schedulerUseCase optionaler Use Case zur Scheduler-Steuerung; + * {@code null} wird als leer behandelt + * @param isConfigDirty Supplier der {@code true} zurückgibt wenn der + * Konfigurationseditor ungespeicherte Änderungen hat; + * {@code null} wird als immer {@code false} behandelt + */ + public GuiSchedulerTab( + Optional schedulerUseCase, + Supplier isConfigDirty) { + this.schedulerUseCase = schedulerUseCase == null ? Optional.empty() : schedulerUseCase; + this.isConfigDirty = isConfigDirty != null ? isConfigDirty : () -> false; + tab.setClosable(false); + buildUi(); + applyInitialState(); + } + + /** + * Liefert den JavaFX-Tab-Knoten für den Einhang in das {@code TabPane}. + * + * @return Tab-Knoten; nie {@code null} + */ + public Tab tab() { + return tab; + } + + /** + * Aktualisiert alle Tab-Elemente anhand des aktuellen Scheduler-Status. + *

+ * Wird von der {@link GuiStatusRefreshTimeline} im Sekundentakt auf dem + * JavaFX Application Thread aufgerufen. Implementiert alle in der Spezifikation + * definierten Button-Zustände, Label-Texte und Sichtbarkeitsregeln. + * + * @param status aktueller Scheduler-Status; darf nicht {@code null} sein + */ + public void updateStatus(SchedulerStatus status) { + updateStatusLabel(status); + updateButtons(status); + updateNextTickLabel(status); + updateLastRunLabel(status); + updateLastErrorLabel(status); + updateIntervalFieldEditability(status); + updateAutostartBanner(status); + } + + // ------------------------------------------------------------------------- + // UI-Aufbau + // ------------------------------------------------------------------------- + + private void buildUi() { + buildAutostartBanner(); + VBox controlArea = buildControlArea(); + VBox content = new VBox(0, autostartErrorBanner, controlArea); + tab.setContent(content); + wireActions(); + } + + private void buildAutostartBanner() { + Label autostartTitleLabel = new Label("⚠ Autostart fehlgeschlagen – Scheduler ist nicht aktiv."); + autostartTitleLabel.setStyle("-fx-font-weight: bold;"); + autostartErrorDetailLabel.setWrapText(true); + HBox bannerButtons = new HBox(10, autostartStartButton, autostartDisableButton); + autostartErrorBanner.getChildren().addAll( + autostartTitleLabel, autostartErrorDetailLabel, bannerButtons); + autostartErrorBanner.setStyle( + "-fx-background-color: #fff3cd; -fx-border-color: #ffc107;" + + " -fx-border-width: 1; -fx-padding: 10;"); + autostartErrorBanner.setVisible(false); + autostartErrorBanner.setManaged(false); + } + + private VBox buildControlArea() { + statusLabel.setStyle("-fx-font-size: 14px; -fx-font-weight: bold; -fx-text-fill: #7f8c8d;"); + + stopButton.setDisable(true); + HBox buttonBox = new HBox(10, startButton, stopButton); + + nextTickLabel.setVisible(false); + nextTickLabel.setManaged(false); + + lastRunLabel.setWrapText(true); + + lastErrorLabel.setStyle("-fx-text-fill: #c0392b;"); + lastErrorLabel.setWrapText(true); + lastErrorLabel.setVisible(false); + lastErrorLabel.setManaged(false); + + Label intervalLabel = new Label("Intervall (Sekunden):"); + intervalField.setPrefColumnCount(10); + HBox intervalBox = new HBox(10, intervalLabel, intervalField); + intervalBox.setAlignment(Pos.CENTER_LEFT); + + intervalValidationLabel.setStyle("-fx-text-fill: #c0392b; -fx-font-size: 11px;"); + intervalValidationLabel.setWrapText(true); + intervalValidationLabel.setVisible(false); + intervalValidationLabel.setManaged(false); + + VBox controlArea = new VBox(12, + statusLabel, + buttonBox, + nextTickLabel, + lastRunLabel, + lastErrorLabel, + new Separator(), + intervalBox, + intervalValidationLabel); + controlArea.setPadding(new Insets(16)); + return controlArea; + } + + private void wireActions() { + startButton.setOnAction(e -> executeStart()); + stopButton.setOnAction(e -> executeStop()); + autostartStartButton.setOnAction(e -> executeStart()); + autostartDisableButton.setOnAction(e -> executeDisableAutostart()); + + intervalField.focusedProperty().addListener((obs, wasFocused, focused) -> { + if (!focused) { + validateAndSaveInterval(); + } + }); + } + + private void applyInitialState() { + if (schedulerUseCase.isEmpty()) { + startButton.setDisable(true); + startButton.setTooltip(new Tooltip("Anwendung nicht laufbereit")); + stopButton.setDisable(true); + intervalField.setEditable(false); + intervalField.setDisable(true); + } else { + intervalField.setText(String.valueOf(schedulerUseCase.get().getIntervalSeconds())); + } + } + + // ------------------------------------------------------------------------- + // updateStatus-Hilfsmethoden + // ------------------------------------------------------------------------- + + private void updateStatusLabel(SchedulerStatus status) { + switch (status.state()) { + case STOPPED -> { + statusLabel.setText("○ Gestoppt"); + statusLabel.setStyle("-fx-font-size: 14px; -fx-font-weight: bold; -fx-text-fill: #7f8c8d;"); + } + case STARTING -> { + statusLabel.setText("⟳ Wird gestartet…"); + statusLabel.setStyle("-fx-font-size: 14px; -fx-font-weight: bold; -fx-text-fill: #e67e22;"); + } + case RUNNING_IDLE -> { + statusLabel.setText("● Aktiv"); + statusLabel.setStyle("-fx-font-size: 14px; -fx-font-weight: bold; -fx-text-fill: #27ae60;"); + } + case RUNNING_BATCH_ACTIVE -> { + statusLabel.setText("● Aktiv – Lauf aktiv"); + statusLabel.setStyle("-fx-font-size: 14px; -fx-font-weight: bold; -fx-text-fill: #27ae60;"); + } + case STOPPING_BATCH_ACTIVE -> { + statusLabel.setText("○ Gestoppt – aktueller Lauf läuft noch"); + statusLabel.setStyle("-fx-font-size: 14px; -fx-font-weight: bold; -fx-text-fill: #7f8c8d;"); + } + } + } + + private void updateButtons(SchedulerStatus status) { + boolean noUseCase = schedulerUseCase.isEmpty(); + boolean configDirty = Boolean.TRUE.equals(isConfigDirty.get()); + + switch (status.state()) { + case STOPPED -> { + stopButton.setDisable(true); + if (noUseCase) { + startButton.setDisable(true); + startButton.setTooltip(new Tooltip("Anwendung nicht laufbereit")); + } else if (configDirty) { + startButton.setDisable(true); + startButton.setTooltip(new Tooltip("Bitte Konfiguration speichern")); + } else { + startButton.setDisable(false); + startButton.setTooltip(null); + } + } + case STARTING -> { + startButton.setDisable(true); + stopButton.setDisable(true); + } + case RUNNING_IDLE, RUNNING_BATCH_ACTIVE -> { + startButton.setDisable(true); + startButton.setTooltip(null); + stopButton.setDisable(false); + } + case STOPPING_BATCH_ACTIVE -> { + startButton.setDisable(true); + stopButton.setDisable(true); + } + } + autostartStartButton.setDisable(startButton.isDisable()); + } + + private void updateNextTickLabel(SchedulerStatus status) { + if (status.state() == SchedulerState.RUNNING_IDLE && status.nextTickAt().isPresent()) { + long remaining = ChronoUnit.SECONDS.between(Instant.now(), status.nextTickAt().get()); + if (remaining > 0) { + long minutes = remaining / 60; + long seconds = remaining % 60; + nextTickLabel.setText(String.format("Nächster Lauf in: %02d:%02d", minutes, seconds)); + } else { + nextTickLabel.setText("Lauf steht bevor…"); + } + nextTickLabel.setVisible(true); + nextTickLabel.setManaged(true); + } else { + nextTickLabel.setVisible(false); + nextTickLabel.setManaged(false); + } + } + + private void updateLastRunLabel(SchedulerStatus status) { + if (status.lastRunEndedAt().isPresent() && status.lastRunSummary().isPresent()) { + Instant endedAt = status.lastRunEndedAt().get(); + RunSummary summary = status.lastRunSummary().get(); + String timeStr = TIME_FORMATTER.format(endedAt); + boolean noDocuments = summary.successCount() == 0 + && summary.failedCount() == 0 + && summary.skippedCount() == 0; + if (noDocuments) { + lastRunLabel.setText("Letzter Lauf: " + timeStr + " – keine neuen Dokumente"); + } else { + lastRunLabel.setText("Letzter Lauf: " + timeStr + " – " + + summary.successCount() + " Dokumente verarbeitet"); + } + } else { + lastRunLabel.setText("Noch kein Lauf in dieser Sitzung."); + } + } + + private void updateLastErrorLabel(SchedulerStatus status) { + Optional lastError = status.lastError(); + if (lastError.isPresent() && !lastError.get().isBlank()) { + lastErrorLabel.setText("Fehler: " + lastError.get()); + lastErrorLabel.setVisible(true); + lastErrorLabel.setManaged(true); + } else { + lastErrorLabel.setVisible(false); + lastErrorLabel.setManaged(false); + } + } + + private void updateIntervalFieldEditability(SchedulerStatus status) { + boolean editable = status.state() == SchedulerState.STOPPED + && schedulerUseCase.isPresent() + && !Boolean.TRUE.equals(isConfigDirty.get()); + intervalField.setEditable(editable); + intervalField.setDisable(!editable); + } + + private void updateAutostartBanner(SchedulerStatus status) { + boolean show = status.autostartFailed() + && !autostartBannerDismissed + && !status.state().isActive(); + autostartErrorBanner.setVisible(show); + autostartErrorBanner.setManaged(show); + if (show) { + status.lastError().ifPresent(autostartErrorDetailLabel::setText); + } + } + + // ------------------------------------------------------------------------- + // Aktions-Handler + // ------------------------------------------------------------------------- + + private void executeStart() { + LOG.info("GUI: Scheduler-Start angefordert."); + startButton.setDisable(true); + autostartStartButton.setDisable(true); + stopButton.setDisable(true); + workerExecutor.submit(() -> schedulerUseCase.ifPresent(uc -> { + try { + uc.start(); + LOG.info("GUI: Scheduler erfolgreich gestartet."); + } catch (SchedulerStartException e) { + LOG.warn("GUI: Scheduler-Start fehlgeschlagen: {}", e.getMessage()); + Platform.runLater(() -> showStartErrorAlert(e.getMessage())); + } catch (RuntimeException e) { + LOG.error("GUI: Unerwarteter Fehler beim Starten des Schedulers.", e); + Platform.runLater(() -> showStartErrorAlert("Unerwarteter Fehler: " + e.getMessage())); + } + })); + } + + private void executeStop() { + LOG.info("GUI: Scheduler-Stopp angefordert."); + startButton.setDisable(true); + stopButton.setDisable(true); + workerExecutor.submit(() -> schedulerUseCase.ifPresent(uc -> { + try { + uc.stop(); + LOG.info("GUI: Scheduler gestoppt."); + } catch (RuntimeException e) { + LOG.error("GUI: Unerwarteter Fehler beim Stoppen des Schedulers.", e); + } + })); + } + + private void executeDisableAutostart() { + LOG.info("GUI: Autostart-Deaktivierung angefordert."); + autostartDisableButton.setDisable(true); + workerExecutor.submit(() -> schedulerUseCase.ifPresent(uc -> { + try { + uc.disableAutostart(); + LOG.info("GUI: Autostart erfolgreich deaktiviert."); + Platform.runLater(() -> { + autostartBannerDismissed = true; + autostartErrorBanner.setVisible(false); + autostartErrorBanner.setManaged(false); + autostartDisableButton.setDisable(false); + }); + } catch (RuntimeException e) { + LOG.error("GUI: Fehler beim Deaktivieren des Autostarts.", e); + Platform.runLater(() -> autostartDisableButton.setDisable(false)); + } + })); + } + + private void validateAndSaveInterval() { + String text = intervalField.getText() == null ? "" : intervalField.getText().trim(); + try { + int value = Integer.parseInt(text); + if (value < MIN_INTERVAL_SECONDS) { + showIntervalValidationError( + "Mindestintervall ist " + MIN_INTERVAL_SECONDS + " Sekunden."); + } else { + hideIntervalValidationError(); + workerExecutor.submit(() -> schedulerUseCase.ifPresent(uc -> { + try { + uc.saveIntervalSeconds(value); + } catch (RuntimeException e) { + LOG.warn("GUI: Fehler beim Speichern des Scheduler-Intervalls: {}", e.getMessage()); + Platform.runLater(() -> showIntervalValidationError( + "Speichern fehlgeschlagen: " + e.getMessage())); + } + })); + } + } catch (NumberFormatException e) { + showIntervalValidationError("Bitte eine ganze Zahl eingeben."); + } + } + + private void showIntervalValidationError(String message) { + intervalValidationLabel.setText(message); + intervalValidationLabel.setVisible(true); + intervalValidationLabel.setManaged(true); + } + + private void hideIntervalValidationError() { + intervalValidationLabel.setVisible(false); + intervalValidationLabel.setManaged(false); + } + + private static void showStartErrorAlert(String message) { + Alert alert = new Alert(Alert.AlertType.ERROR); + alert.setTitle("Scheduler-Start fehlgeschlagen"); + alert.setHeaderText("Der Scheduler konnte nicht gestartet werden."); + alert.setContentText(message != null ? message : "Unbekannter Fehler."); + alert.showAndWait(); + } +} diff --git a/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiAdapterSmokeTest.java b/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiAdapterSmokeTest.java index 9009444..4004608 100644 --- a/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiAdapterSmokeTest.java +++ b/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiAdapterSmokeTest.java @@ -244,16 +244,18 @@ class GuiAdapterSmokeTest { "The 'Speichern' button must be visible"); assertEquals("Speichern unter", workspace.saveAsButton().getText(), "The 'Speichern unter' button must be visible"); - assertEquals(4, workspace.tabPane().getTabs().size(), - "Configuration tab, processing-run tab, history tab and prompt editor tab must all be present"); + assertEquals(5, workspace.tabPane().getTabs().size(), + "Configuration tab, processing-run tab, scheduler tab, history tab and prompt editor tab must all be present"); assertEquals("Konfiguration", workspace.tabPane().getTabs().get(0).getText(), "The first tab must use the configuration label"); assertEquals("Verarbeitungslauf", workspace.tabPane().getTabs().get(1).getText(), "The second tab must host the processing-run view"); - assertEquals("Verlauf", workspace.tabPane().getTabs().get(2).getText(), - "The third tab must host the history view"); - assertEquals("Prompt", workspace.tabPane().getTabs().get(3).getText(), - "The fourth tab must host the prompt editor"); + assertEquals("Scheduler", workspace.tabPane().getTabs().get(2).getText(), + "The third tab must host the scheduler control"); + assertEquals("Verlauf", workspace.tabPane().getTabs().get(3).getText(), + "The fourth tab must host the history view"); + assertEquals("Prompt", workspace.tabPane().getTabs().get(4).getText(), + "The fifth tab must host the prompt editor"); assertEquals( "Pfade,Provider,Verarbeitungslimits,Tests,Meldungen", String.join(",", workspace.sectionTitles()), diff --git a/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/in/SchedulerControlUseCase.java b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/in/SchedulerControlUseCase.java index e17b1ca..ac33c2f 100644 --- a/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/in/SchedulerControlUseCase.java +++ b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/in/SchedulerControlUseCase.java @@ -60,4 +60,40 @@ public interface SchedulerControlUseCase { * @return aktueller Scheduler-Status; nie {@code null} */ SchedulerStatus getStatus(); + + /** + * Gibt das aktuell konfigurierte Ausführungsintervall in Sekunden zurück. + *

+ * Wird vom Scheduler-Tab genutzt, um den Initialwert des Intervall-Feldes + * anzuzeigen. Der Wert entspricht dem beim Start der Anwendung geladenen + * Konfigurationswert (mindestens 30 Sekunden). + * + * @return Intervall in Sekunden; immer ≥ 30 + */ + int getIntervalSeconds(); + + /** + * Persistiert das Ausführungsintervall in die Konfigurationsdatei. + *

+ * Sicher nur aufzurufen wenn der Scheduler gestoppt ist. Der in-Memory-Wert + * wird nicht aktualisiert; der neue Wert wird beim nächsten Anwendungsstart + * gelesen. + *

+ * Muss auf einem Hintergrund-Thread aufgerufen werden, da der Schreibvorgang + * den Konfigurations-Datei-Lock erwerben muss. + * + * @param seconds Intervall in Sekunden; sollte ≥ 30 sein + */ + void saveIntervalSeconds(int seconds); + + /** + * Deaktiviert den Autostart durch Persistieren von {@code scheduler.enabled=false}. + *

+ * Wird vom Scheduler-Tab aufgerufen, wenn der Benutzer den fehlgeschlagenen + * Autostart dauerhaft deaktivieren möchte. Sicher aufzurufen wenn der Scheduler + * gestoppt ist. + *

+ * Muss auf einem Hintergrund-Thread aufgerufen werden. + */ + void disableAutostart(); } diff --git a/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/usecase/DefaultSchedulerControlUseCase.java b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/usecase/DefaultSchedulerControlUseCase.java index 871f6be..0d279a3 100644 --- a/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/usecase/DefaultSchedulerControlUseCase.java +++ b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/usecase/DefaultSchedulerControlUseCase.java @@ -197,6 +197,42 @@ public class DefaultSchedulerControlUseCase implements SchedulerControlUseCase { return statusRef.get(); } + /** + * Gibt das beim Start der Anwendung geladene Ausführungsintervall in Sekunden zurück. + * + * @return Intervall in Sekunden; immer ≥ 30 + */ + @Override + public int getIntervalSeconds() { + return intervalSeconds; + } + + /** + * Persistiert das Ausführungsintervall in die Konfigurationsdatei. + *

+ * Der in-Memory-Wert wird nicht aktualisiert; der neue Wert wird beim + * nächsten Anwendungsstart gelesen. + * + * @param seconds Intervall in Sekunden + */ + @Override + public void saveIntervalSeconds(int seconds) { + settingsPort.saveIntervalSeconds(seconds); + } + + /** + * Deaktiviert den Autostart durch Persistieren von {@code scheduler.enabled=false}. + */ + @Override + public void disableAutostart() { + try { + settingsPort.saveEnabled(false); + logger.info("Autostart deaktiviert."); + } catch (Exception e) { + logger.warn("Fehler beim Deaktivieren des Autostarts.", e); + } + } + /** * Markiert den Autostart als fehlgeschlagen. *