Schritt 10: GuiSchedulerTab implementieren und in Workspace verdrahten

- 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 <noreply@anthropic.com>
This commit is contained in:
2026-05-06 16:05:24 +02:00
parent 0cec9347c1
commit fa4f327a3f
5 changed files with 569 additions and 10 deletions
@@ -459,13 +459,19 @@ public final class GuiConfigurationEditorWorkspace {
private final GuiBatchRunTab batchRunTab; 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. * erstellt und in den {@link #tabPane} eingehängt.
*/ */
private final de.gecheckt.pdf.umbenenner.adapter.in.gui.history.GuiHistoryTab historyTab; 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. * und in den {@link #tabPane} eingehängt.
*/ */
private final GuiPromptEditorTab promptEditorTab; private final GuiPromptEditorTab promptEditorTab;
@@ -557,6 +563,10 @@ public final class GuiConfigurationEditorWorkspace {
this::editorTargetFolder, this::editorTargetFolder,
effectiveContext.configurationFileLockPort()); effectiveContext.configurationFileLockPort());
this.schedulerTab = new GuiSchedulerTab(
effectiveContext.schedulerControlUseCase(),
() -> editorState.isDirty());
this.historyTab = new de.gecheckt.pdf.umbenenner.adapter.in.gui.history.GuiHistoryTab( this.historyTab = new de.gecheckt.pdf.umbenenner.adapter.in.gui.history.GuiHistoryTab(
effectiveContext.historyOverviewPort(), effectiveContext.historyOverviewPort(),
effectiveContext.historyDetailsPort(), effectiveContext.historyDetailsPort(),
@@ -1091,14 +1101,15 @@ public final class GuiConfigurationEditorWorkspace {
* <ul> * <ul>
* <li>{@link de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun.GuiBatchRunTab#updateSchedulerState} * <li>{@link de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun.GuiBatchRunTab#updateSchedulerState}
* Schaltflächen-Zustand im Verarbeitungslauf-Tab</li> * Schaltflächen-Zustand im Verarbeitungslauf-Tab</li>
* <li>{@link GuiSchedulerTab#updateStatus} Statusanzeige im Scheduler-Tab</li>
* <li>{@link #updateLockState} Banner und Speichern-Button im Konfig-Tab</li> * <li>{@link #updateLockState} Banner und Speichern-Button im Konfig-Tab</li>
* </ul> * </ul>
* Der Scheduler-Tab wird in einem späteren Implementierungsschritt hinzugefügt.
* *
* @param status aktueller Scheduler-Status; darf nicht {@code null} sein * @param status aktueller Scheduler-Status; darf nicht {@code null} sein
*/ */
public void onSchedulerStatusRefresh(SchedulerStatus status) { public void onSchedulerStatusRefresh(SchedulerStatus status) {
batchRunTab.updateSchedulerState(status); batchRunTab.updateSchedulerState(status);
schedulerTab.updateStatus(status);
updateLockState(status); updateLockState(status);
} }
@@ -1655,7 +1666,7 @@ public final class GuiConfigurationEditorWorkspace {
scrollPane.setPadding(new Insets(0)); scrollPane.setPadding(new Insets(0));
editorTab.setContent(scrollPane); 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); root.setCenter(tabPane);
// Tab-Wechsel-Schutz: Beim Wechsel weg vom Verarbeitungslauf-Tab prüfen ob // Tab-Wechsel-Schutz: Beim Wechsel weg vom Verarbeitungslauf-Tab prüfen ob
@@ -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.
* <p>
* 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.
*
* <h2>Bereiche</h2>
* <ul>
* <li><strong>Autostart-Fehler-Banner</strong>: Sichtbar wenn beim Programmstart
* ein konfigurierter Autostart fehlgeschlagen ist. Bietet Schnellaktionen zum
* Starten des Schedulers oder zum Deaktivieren des Autostarts.</li>
* <li><strong>Scheduler-Steuerung</strong>: Status-Anzeige (● Aktiv / ○ Gestoppt),
* Start-/Stopp-Schaltflächen, Countdown bis zum nächsten Lauf,
* Letzter-Lauf-Info, Fehlermeldung und Intervall-Konfiguration.</li>
* </ul>
*
* <h2>Threading</h2>
* <p>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<SchedulerControlUseCase> schedulerUseCase;
private final Supplier<Boolean> 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<SchedulerControlUseCase> schedulerUseCase,
Supplier<Boolean> 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.
* <p>
* 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<String> 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();
}
}
@@ -244,16 +244,18 @@ class GuiAdapterSmokeTest {
"The 'Speichern' button must be visible"); "The 'Speichern' button must be visible");
assertEquals("Speichern unter", workspace.saveAsButton().getText(), assertEquals("Speichern unter", workspace.saveAsButton().getText(),
"The 'Speichern unter' button must be visible"); "The 'Speichern unter' button must be visible");
assertEquals(4, workspace.tabPane().getTabs().size(), assertEquals(5, workspace.tabPane().getTabs().size(),
"Configuration tab, processing-run tab, history tab and prompt editor tab must all be present"); "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(), assertEquals("Konfiguration", workspace.tabPane().getTabs().get(0).getText(),
"The first tab must use the configuration label"); "The first tab must use the configuration label");
assertEquals("Verarbeitungslauf", workspace.tabPane().getTabs().get(1).getText(), assertEquals("Verarbeitungslauf", workspace.tabPane().getTabs().get(1).getText(),
"The second tab must host the processing-run view"); "The second tab must host the processing-run view");
assertEquals("Verlauf", workspace.tabPane().getTabs().get(2).getText(), assertEquals("Scheduler", workspace.tabPane().getTabs().get(2).getText(),
"The third tab must host the history view"); "The third tab must host the scheduler control");
assertEquals("Prompt", workspace.tabPane().getTabs().get(3).getText(), assertEquals("Verlauf", workspace.tabPane().getTabs().get(3).getText(),
"The fourth tab must host the prompt editor"); "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( assertEquals(
"Pfade,Provider,Verarbeitungslimits,Tests,Meldungen", "Pfade,Provider,Verarbeitungslimits,Tests,Meldungen",
String.join(",", workspace.sectionTitles()), String.join(",", workspace.sectionTitles()),
@@ -60,4 +60,40 @@ public interface SchedulerControlUseCase {
* @return aktueller Scheduler-Status; nie {@code null} * @return aktueller Scheduler-Status; nie {@code null}
*/ */
SchedulerStatus getStatus(); SchedulerStatus getStatus();
/**
* Gibt das aktuell konfigurierte Ausführungsintervall in Sekunden zurück.
* <p>
* 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 &ge; 30
*/
int getIntervalSeconds();
/**
* Persistiert das Ausführungsintervall in die Konfigurationsdatei.
* <p>
* Sicher nur aufzurufen wenn der Scheduler gestoppt ist. Der in-Memory-Wert
* wird nicht aktualisiert; der neue Wert wird beim nächsten Anwendungsstart
* gelesen.
* <p>
* Muss auf einem Hintergrund-Thread aufgerufen werden, da der Schreibvorgang
* den Konfigurations-Datei-Lock erwerben muss.
*
* @param seconds Intervall in Sekunden; sollte &ge; 30 sein
*/
void saveIntervalSeconds(int seconds);
/**
* Deaktiviert den Autostart durch Persistieren von {@code scheduler.enabled=false}.
* <p>
* Wird vom Scheduler-Tab aufgerufen, wenn der Benutzer den fehlgeschlagenen
* Autostart dauerhaft deaktivieren möchte. Sicher aufzurufen wenn der Scheduler
* gestoppt ist.
* <p>
* Muss auf einem Hintergrund-Thread aufgerufen werden.
*/
void disableAutostart();
} }
@@ -197,6 +197,42 @@ public class DefaultSchedulerControlUseCase implements SchedulerControlUseCase {
return statusRef.get(); return statusRef.get();
} }
/**
* Gibt das beim Start der Anwendung geladene Ausführungsintervall in Sekunden zurück.
*
* @return Intervall in Sekunden; immer &ge; 30
*/
@Override
public int getIntervalSeconds() {
return intervalSeconds;
}
/**
* Persistiert das Ausführungsintervall in die Konfigurationsdatei.
* <p>
* 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. * Markiert den Autostart als fehlgeschlagen.
* <p> * <p>