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 fb79cbb..a4463b5 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 @@ -18,6 +18,7 @@ import de.gecheckt.pdf.umbenenner.adapter.in.gui.history.GuiHistoryResetDocument import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiConfigurationEditorState; import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiConfigurationEditorStateFactory; import de.gecheckt.pdf.umbenenner.application.port.in.ResetDocumentStatusResult; +import de.gecheckt.pdf.umbenenner.application.port.in.SchedulerControlUseCase; import de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.AiModelCatalogPort; import de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.EffectiveApiKeyDescriptor; import de.gecheckt.pdf.umbenenner.application.validation.editor.ApiKeyResolutionPort; @@ -48,14 +49,19 @@ import de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint; * the {@link GuiManualFileCopyPort} used to manually copy a source file to the target * folder for documents that have not yet been successfully processed, and * the {@link GuiHistoricalDocumentContextPort} used to retrieve the historical processing - * context for documents that were skipped in the current run, and the resolved application - * version string that the status bar displays at the bottom of the main window. + * context for documents that were skipped in the current run, the resolved application + * version string that the status bar displays at the bottom of the main window, and the + * optional {@link SchedulerControlUseCase} for controlling the automatic scheduler. *
* The optional {@code applicationContextError} carries a human-readable German error * message when the bootstrap-side application run context could not be initialised at * startup (e.g., invalid or incomplete configuration). An empty value signals that the * run context was built successfully and batch runs can be launched immediately. *
+ * The optional {@code schedulerControlUseCase} is present when the automatic scheduler + * was successfully wired at startup. An empty value means scheduler control is not + * available in this startup context (e.g., no valid configuration was loaded at startup). + *
* 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.
*/
@@ -84,7 +90,8 @@ public record GuiStartupContext(
GuiDeleteDocumentHistoryPort deleteDocumentHistoryPort,
GuiPromptEditorPortFactory promptEditorPortFactory,
GuiCreateNewDatabasePort createNewDatabasePort,
- Optional
+ * Preserves existing callers that were written before the scheduler was added.
+ *
+ * @param initialState initial editor state; must not be {@code null}
+ * @param startupNotice optional startup notice; {@code null} becomes empty
+ * @param configurationFileLoader file-loading callback; must not be {@code null}
+ * @param configurationFileWriter file-writing callback; must not be {@code null}
+ * @param modelCatalogPort port for retrieving AI model lists; must not be {@code null}
+ * @param apiKeyResolutionPort port for resolving API key provenance; must not be {@code null}
+ * @param providerTechnicalTestService service for provider-specific technical checks; must not be {@code null}
+ * @param pathCheckPort port for filesystem path accessibility checks; must not be {@code null}
+ * @param technicalTestOrchestrator orchestrator for the full technical test run; must not be {@code null}
+ * @param correctionExecutionService service for executing confirmed corrective actions; must not be {@code null}
+ * @param batchRunLauncher bridge that executes a regular batch run; must not be {@code null}
+ * @param miniRunLauncher bridge that executes a targeted mini-run; must not be {@code null}
+ * @param resetDocumentStatusPort bridge that resets document status; must not be {@code null}
+ * @param manualFileRenamePort bridge that renames a target file; must not be {@code null}
+ * @param manualFileCopyPort bridge that copies a source file; must not be {@code null}
+ * @param historicalDocumentContextPort bridge for historical processing context; must not be {@code null}
+ * @param applicationVersion resolved application version string; {@code null} defaults to {@code "dev"}
+ * @param promptEditorPort bridge zum Prompt-Editor-Use-Case; must not be {@code null}
+ * @param historyOverviewPort bridge for history overview; must not be {@code null}
+ * @param historyDetailsPort bridge for history details; must not be {@code null}
+ * @param historyResetDocumentStatusPort bridge for history reset; must not be {@code null}
+ * @param deleteDocumentHistoryPort bridge for history deletion; must not be {@code null}
+ * @param promptEditorPortFactory factory for prompt editor ports; must not be {@code null}
+ * @param createNewDatabasePort bridge for new database creation; must not be {@code null}
+ * @param applicationContextError optional error from context init; {@code null} becomes empty
+ */
+ public GuiStartupContext(
+ GuiConfigurationEditorState initialState,
+ Optional
+ * Wird von der Bootstrap-Schicht aufgerufen, wenn ein konfigurierter Autostart
+ * beim Programmstart fehlgeschlagen ist. Alle übrigen Statusfelder bleiben erhalten;
+ * lediglich {@link SchedulerStatus#autostartFailed()} wird auf {@code true} gesetzt.
+ *
+ * Diese Methode darf nur aufgerufen werden, wenn der Scheduler noch gestoppt ist
+ * (unmittelbar nach einem fehlgeschlagenen {@link #start()}).
+ */
+ public void markAutostartFailed() {
+ statusRef.updateAndGet(s -> new SchedulerStatus(
+ s.state(), s.lastRunEndedAt(), s.lastRunSummary(),
+ s.nextTickAt(), s.lastError(), true));
+ logger.warn("Scheduler-Status: Autostart als fehlgeschlagen markiert.");
+ }
+
// -------------------------------------------------------------------------
// Tick-Wrapper (package-private für Testbarkeit)
// -------------------------------------------------------------------------
diff --git a/pdf-umbenenner-bootstrap/pom.xml b/pdf-umbenenner-bootstrap/pom.xml
index 81948ae..4ae2e75 100644
--- a/pdf-umbenenner-bootstrap/pom.xml
+++ b/pdf-umbenenner-bootstrap/pom.xml
@@ -36,6 +36,11 @@
+ * Wird beim Start via {@link #tryInitializeScheduler(Path)} gesetzt. Leer, wenn
+ * beim Start keine gültige Konfiguration vorlag oder die Scheduler-Initialisierung
+ * fehlschlug. {@code volatile} sichert die Sichtbarkeit über Thread-Grenzen hinweg.
+ */
+ private volatile Optional
@@ -761,9 +780,28 @@ public class BootstrapRunner {
} catch (Exception e) {
LOG.error("GUI startup failed: {}", e.getMessage(), e);
return 1;
+ } finally {
+ stopGuiSchedulerIfActive();
}
}
+ /**
+ * Stoppt den aktiven Scheduler, wenn die GUI beendet wird.
+ *
+ * Wird im {@code finally}-Block von {@link #startGuiMode(Optional)} aufgerufen,
+ * damit der Scheduler-Thread ordentlich beendet wird, bevor der JVM-Prozess endet.
+ * Ist kein Scheduler aktiv oder bereits gestoppt, hat dieser Aufruf keine Wirkung.
+ */
+ private void stopGuiSchedulerIfActive() {
+ guiSchedulerUseCase.ifPresent(scheduler -> {
+ if (scheduler.getStatus().state().isActive()) {
+ LOG.info("GUI-Beendigung: laufender Scheduler wird gestoppt.");
+ scheduler.stop();
+ }
+ });
+ guiSchedulerUseCase = Optional.empty();
+ }
+
/**
* Runs the headless batch processing pipeline for the given startup arguments.
*
@@ -936,13 +974,19 @@ public class BootstrapRunner {
GuiPromptEditorPort promptEditorPort = buildGuiPromptEditorPort(
loadedState.values().promptTemplateFile());
Optional
+ * Erzeugt {@link FileChannelConfigurationAccessAdapter}, {@link ScheduledExecutorServiceSchedulerAdapter}
+ * und {@link DefaultSchedulerControlUseCase} und speichert den Use Case in
+ * {@link #guiSchedulerUseCase}. Ist in der Konfiguration {@code scheduler.enabled=true}
+ * gesetzt, wird der Scheduler sofort gestartet (Autostart).
+ *
+ * Schlägt die Initialisierung fehl, wird {@link #guiSchedulerUseCase} auf
+ * {@link Optional#empty()} gesetzt und der Fehler als Warnung geloggt.
+ * Der GUI-Start wird dadurch nicht abgebrochen.
+ *
+ * @param configFilePath Pfad zur Konfigurationsdatei; muss auf Platte existieren
+ */
+ private void tryInitializeScheduler(Path configFilePath) {
+ try {
+ FileChannelConfigurationAccessAdapter configAccessAdapter =
+ new FileChannelConfigurationAccessAdapter(configFilePath);
+ ScheduledExecutorServiceSchedulerAdapter schedulerAdapter =
+ new ScheduledExecutorServiceSchedulerAdapter(
+ result -> LOG.debug("Scheduler-Tick-Ergebnis: {}",
+ result.getClass().getSimpleName()));
+ BatchRunTrigger batchRunTrigger = () -> {
+ Optional