diff --git a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiApplicationContextInitializer.java b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiApplicationContextInitializer.java
new file mode 100644
index 0000000..2d42728
--- /dev/null
+++ b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiApplicationContextInitializer.java
@@ -0,0 +1,66 @@
+package de.gecheckt.pdf.umbenenner.adapter.in.gui;
+
+import java.nio.file.Path;
+import java.util.Optional;
+
+import de.gecheckt.pdf.umbenenner.application.port.in.SchedulerControlUseCase;
+
+/**
+ * Callback invoked by the workspace on a background thread after a configuration file
+ * has been successfully loaded from disk.
+ *
+ * Bootstrap supplies an implementation that builds the application run context
+ * (migrate → load → validate → schema-init sequence) and, on success, also initialises
+ * the automatic scheduler. The workspace calls this initializer inside the same
+ * background submit that loads the editor state, so the JavaFX Application Thread is
+ * never blocked.
+ *
+ * In isolated GUI tests a {@link #noOp() no-op} implementation can be used so that no
+ * Bootstrap wiring is required.
+ */
+@FunctionalInterface
+public interface GuiApplicationContextInitializer {
+
+ /**
+ * Attempts to initialise the application run context for the supplied configuration file.
+ *
+ * If context initialisation succeeds and the configuration enables the scheduler, the
+ * scheduler is also wired and its use case is returned in the result. The caller is
+ * responsible for handing the scheduler use case to the scheduler tab on the JavaFX
+ * Application Thread via {@code Platform.runLater}.
+ *
+ * This method must be called on a background worker thread, not on the JavaFX Application
+ * Thread.
+ *
+ * @param configFilePath path to the {@code .properties} configuration file; must exist on disk
+ * @return the result of the initialisation attempt; never {@code null}
+ */
+ InitResult initialize(Path configFilePath);
+
+ /**
+ * Returns a no-op initializer that always reports success and no scheduler.
+ *
+ * Suitable for GUI tests and startup paths where no Bootstrap wiring is available.
+ *
+ * @return no-op initializer; never {@code null}
+ */
+ static GuiApplicationContextInitializer noOp() {
+ return configFilePath -> new InitResult(Optional.empty(), Optional.empty());
+ }
+
+ /**
+ * Result of a context initialisation attempt.
+ *
+ * @param contextError empty on success; a human-readable German error message
+ * when initialisation failed — the GUI remains functional
+ * but falls back to per-run initialisation for batch runs;
+ * must not be {@code null}
+ * @param schedulerControlUseCase the scheduler use case when the configuration enables the
+ * scheduler and initialisation succeeded; empty otherwise;
+ * must not be {@code null}
+ */
+ record InitResult(
+ Optional contextError,
+ Optional schedulerControlUseCase) {
+ }
+}
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 85e674f..588b3ae 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
@@ -436,6 +436,13 @@ public final class GuiConfigurationEditorWorkspace {
*/
private final GuiCreateNewDatabasePort createNewDatabasePort;
+ /**
+ * Callback, der nach jedem erfolgreichen Datei-Öffnen auf dem Hintergrund-Thread
+ * aufgerufen wird, um den Bootstrap-seitigen Anwendungskontext und den Scheduler
+ * zu initialisieren.
+ */
+ private final GuiApplicationContextInitializer applicationContextInitializer;
+
/**
* Aktiver DB-Busy-Zustand während einer laufenden Datenbank-Anlage. Solange
* dieser Zustand aktiv ist, sind alle DB-lesenden und DB-schreibenden Aktionen
@@ -561,6 +568,7 @@ public final class GuiConfigurationEditorWorkspace {
this.historicalDocumentContextPort = effectiveContext.historicalDocumentContextPort();
this.promptEditorPortFactory = effectiveContext.promptEditorPortFactory();
this.createNewDatabasePort = effectiveContext.createNewDatabasePort();
+ this.applicationContextInitializer = effectiveContext.applicationContextInitializer();
this.batchRunTab = new GuiBatchRunTab(
() -> this.batchRunLauncher,
() -> this.miniRunLauncher,
@@ -1024,7 +1032,13 @@ public final class GuiConfigurationEditorWorkspace {
GuiConfigurationEditorState loadedState = configurationFileLoader.load(configFilePath);
// Speichern des Pfads als letzte geladene Konfiguration
saveLastConfigurationPath(configFilePath);
- Platform.runLater(() -> applyEditorState(loadedState));
+ // Anwendungskontext und Scheduler initialisieren; Ergebnis auf dem FX-Thread auswerten.
+ GuiApplicationContextInitializer.InitResult initResult =
+ applicationContextInitializer.initialize(configFilePath);
+ Platform.runLater(() -> {
+ applyEditorState(loadedState);
+ initResult.schedulerControlUseCase().ifPresent(schedulerTab::onSchedulerAvailable);
+ });
} catch (Exception exception) {
Platform.runLater(() -> showError("Konfiguration konnte nicht geladen werden: "
+ safeMessage(exception)));
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
index bb631fa..ab68fd4 100644
--- 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
@@ -69,7 +69,10 @@ public final class GuiSchedulerTab {
DateTimeFormatter.ofPattern("HH:mm").withZone(ZoneId.systemDefault());
private final Tab tab = new Tab(TAB_TITLE);
- private final Optional schedulerUseCase;
+ // Not final: may be updated via onSchedulerAvailable after the tab was created without a use
+ // case (e.g., when auto-load initialises the scheduler after the workspace was already built).
+ // Declared volatile so worker-thread reads (executeStart/Stop) see the write from the FX thread.
+ private volatile Optional schedulerUseCase;
private final Supplier isConfigDirty;
// -------------------------------------------------------------------------
@@ -134,6 +137,33 @@ public final class GuiSchedulerTab {
return tab;
}
+ /**
+ * Macht den Scheduler-Use-Case für diesen Tab verfügbar, nachdem er nach einem
+ * erfolgreichen Datei-Öffnen initialisiert wurde.
+ *
+ * Wird vom Workspace auf dem JavaFX Application Thread aufgerufen, nachdem der
+ * {@link GuiApplicationContextInitializer} auf einem Hintergrund-Thread einen
+ * {@link SchedulerControlUseCase} geliefert hat. Hat keine Wirkung, wenn bereits
+ * ein Use Case vorhanden ist.
+ *
+ * Muss auf dem JavaFX Application Thread aufgerufen werden.
+ *
+ * @param useCase der neu initialisierte Use Case; darf nicht {@code null} sein
+ */
+ public void onSchedulerAvailable(SchedulerControlUseCase useCase) {
+ if (schedulerUseCase.isPresent()) {
+ return;
+ }
+ schedulerUseCase = Optional.of(useCase);
+ intervalField.setText(String.valueOf(useCase.getIntervalSeconds()));
+ intervalField.setEditable(true);
+ intervalField.setDisable(false);
+ // Buttons werden im nächsten updateStatus-Tick (1 Hz) korrekt gesetzt;
+ // vorab grob aktivieren damit kein misleadender Disabled-Zustand bleibt.
+ startButton.setDisable(false);
+ startButton.setTooltip(null);
+ }
+
/**
* Aktualisiert alle Tab-Elemente anhand des aktuellen Scheduler-Status.
*
diff --git a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiStartupContext.java b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiStartupContext.java
index 9e91caf..027d35c 100644
--- a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiStartupContext.java
+++ b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiStartupContext.java
@@ -70,6 +70,10 @@ import de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint;
* no locking is performed (e.g., no valid configuration was loaded at startup, or locking is
* not required in this context).
*
+ * The {@code applicationContextInitializer} is invoked on a background thread each time the
+ * workspace loads a configuration file (auto-load at startup and manual open). Bootstrap
+ * provides an implementation that builds the application run context and wires the scheduler.
+ *
* All ports and services are supplied by Bootstrap so that the GUI adapter does not need to
* know about provider-specific HTTP details or adapter wiring.
*/
@@ -100,7 +104,8 @@ public record GuiStartupContext(
GuiCreateNewDatabasePort createNewDatabasePort,
Optional applicationContextError,
Optional schedulerControlUseCase,
- Optional configurationFileLockPort) {
+ Optional configurationFileLockPort,
+ GuiApplicationContextInitializer applicationContextInitializer) {
private static final String NO_PROMPT_PORT_MSG = "Kein Prompt-Editor-Port in diesem Startkontext verfügbar.";
private static final String NO_PORT_MSG = "Kein Port in diesem Startkontext.";
@@ -188,6 +193,8 @@ public record GuiStartupContext(
"createNewDatabasePort must not be null");
schedulerControlUseCase = schedulerControlUseCase == null ? Optional.empty() : schedulerControlUseCase;
configurationFileLockPort = configurationFileLockPort == null ? Optional.empty() : configurationFileLockPort;
+ applicationContextInitializer = applicationContextInitializer == null
+ ? GuiApplicationContextInitializer.noOp() : applicationContextInitializer;
}
/**
@@ -255,7 +262,8 @@ public record GuiStartupContext(
historicalDocumentContextPort, applicationVersion, promptEditorPort,
historyOverviewPort, historyDetailsPort, historyResetDocumentStatusPort,
deleteDocumentHistoryPort, promptEditorPortFactory, createNewDatabasePort,
- applicationContextError, Optional.empty(), Optional.empty());
+ applicationContextError, Optional.empty(), Optional.empty(),
+ GuiApplicationContextInitializer.noOp());
}
/**
@@ -326,7 +334,8 @@ public record GuiStartupContext(
historicalDocumentContextPort, applicationVersion, promptEditorPort,
historyOverviewPort, historyDetailsPort, historyResetDocumentStatusPort,
deleteDocumentHistoryPort, promptEditorPortFactory, createNewDatabasePort,
- applicationContextError, schedulerControlUseCase, Optional.empty());
+ applicationContextError, schedulerControlUseCase, Optional.empty(),
+ GuiApplicationContextInitializer.noOp());
}
/**
diff --git a/pdf-umbenenner-bootstrap/src/main/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunner.java b/pdf-umbenenner-bootstrap/src/main/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunner.java
index bb4836d..63962d5 100644
--- a/pdf-umbenenner-bootstrap/src/main/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunner.java
+++ b/pdf-umbenenner-bootstrap/src/main/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunner.java
@@ -21,6 +21,7 @@ import de.gecheckt.pdf.umbenenner.adapter.in.cli.SchedulerBatchCommand;
import de.gecheckt.pdf.umbenenner.adapter.in.scheduler.FileChannelConfigurationAccessAdapter;
import de.gecheckt.pdf.umbenenner.adapter.in.scheduler.ScheduledExecutorServiceSchedulerAdapter;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.GuiAdapter;
+import de.gecheckt.pdf.umbenenner.adapter.in.gui.GuiApplicationContextInitializer;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.GuiConfigurationFileLoader;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.GuiConfigurationFileWriter;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.GuiConfigurationLoadException;
@@ -255,11 +256,13 @@ public class BootstrapRunner {
* the resolved JDBC URL, so that batch runs and reset operations can skip the
* migrate → load → validate → schema-init sequence for each call.
*
- * Written by {@link #initializeApplicationRunContext(Path)} during
- * {@link #buildGuiStartupContext(Optional)} and read by
+ * Written by {@link #initializeApplicationRunContext(Path)} — either directly during
+ * {@link #buildGuiStartupContext(Optional)} (when {@code --config} is supplied and valid),
+ * or via the {@link GuiApplicationContextInitializer} callback that the workspace invokes
+ * on a background thread after each successful file open. Read by
* {@link #launchGuiBatchRun}, {@link #launchGuiMiniBatchRun}, and
- * {@link #resetDocumentStatusForGui}. {@code volatile} ensures visibility
- * across threads without explicit synchronisation on the happy path.
+ * {@link #resetDocumentStatusForGui}. {@code volatile} ensures visibility across threads
+ * without explicit synchronisation on the happy path.
*/
private volatile Optional guiApplicationRunContext = Optional.empty();
@@ -771,8 +774,12 @@ public class BootstrapRunner {
* during startup is treated as a hard GUI startup failure and mapped to exit code 1.
* Normal termination (user closes the window) returns exit code 0.
*
- * The headless batch pipeline is not entered from this method. Configuration loading,
- * schema initialization, and all batch infrastructure are not initialized in the GUI path.
+ * The headless batch pipeline is not entered from this method. When {@code --config} is
+ * supplied and the file exists, configuration loading and schema initialisation run
+ * immediately so the application run context is pre-built. For all other startup paths
+ * (no {@code --config}, missing file, load failure) a {@link GuiApplicationContextInitializer}
+ * callback is wired into the startup context; the workspace calls it on a background thread
+ * each time a configuration file is successfully opened.
*
* @param configPathOverride the optional {@code --config} path string from startup arguments;
* must not be {@code null}
@@ -915,6 +922,20 @@ public class BootstrapRunner {
// Versionsnummer aus dem MANIFEST.MF des gepackten JARs lesen; Fallback "dev" bei IDE-Start
String applicationVersion = ApplicationVersionProvider.resolveVersion();
+ // Initializer that the workspace calls on a background thread after every successful
+ // file open (auto-load at startup and manual open). Builds the ApplicationRunContext
+ // and wires the scheduler for the newly loaded configuration file.
+ GuiApplicationContextInitializer contextInitializer = configFilePath -> {
+ stopGuiSchedulerIfActive();
+ Optional initError = initializeApplicationRunContext(configFilePath);
+ if (initError.isEmpty()) {
+ tryInitializeScheduler(configFilePath);
+ }
+ return new GuiApplicationContextInitializer.InitResult(
+ initError,
+ guiSchedulerUseCase.map(s -> (SchedulerControlUseCase) s));
+ };
+
if (configPathOverride.isEmpty()) {
return new GuiStartupContext(
GuiConfigurationEditorStateFactory.createBlankStartState(),
@@ -941,7 +962,10 @@ public class BootstrapRunner {
deleteHistoryPort,
this::buildGuiPromptEditorPort,
createNewDatabasePort,
- Optional.empty());
+ Optional.empty(),
+ Optional.empty(),
+ Optional.empty(),
+ contextInitializer);
}
Path configPath = Paths.get(configPathOverride.get());
@@ -974,7 +998,10 @@ public class BootstrapRunner {
deleteHistoryPort,
this::buildGuiPromptEditorPort,
createNewDatabasePort,
- Optional.empty());
+ Optional.empty(),
+ Optional.empty(),
+ Optional.empty(),
+ contextInitializer);
}
LOG.info("GUI startup: configuration file confirmed at: {}", configPath.toAbsolutePath());
@@ -997,7 +1024,7 @@ public class BootstrapRunner {
historicalDocumentContextPort, applicationVersion, promptEditorPort,
historyOverviewPort, historyDetailsPort, historyResetPort, deleteHistoryPort,
this::buildGuiPromptEditorPort, createNewDatabasePort, contextError,
- schedulerUseCase, guiRunLockPort);
+ schedulerUseCase, guiRunLockPort, contextInitializer);
} catch (GuiConfigurationLoadException e) {
LOG.error("GUI startup: configuration could not be loaded, starting without it: {}",
e.getMessage(), e);
@@ -1026,7 +1053,10 @@ public class BootstrapRunner {
deleteHistoryPort,
this::buildGuiPromptEditorPort,
createNewDatabasePort,
- Optional.empty());
+ Optional.empty(),
+ Optional.empty(),
+ Optional.empty(),
+ contextInitializer);
}
}