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:
+15
-4
@@ -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
|
||||||
|
|||||||
+474
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
+8
-6
@@ -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()),
|
||||||
|
|||||||
+36
@@ -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 ≥ 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 ≥ 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();
|
||||||
}
|
}
|
||||||
|
|||||||
+36
@@ -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 ≥ 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>
|
||||||
|
|||||||
Reference in New Issue
Block a user