Ergaenze zweiten GUI-Tab fuer Verarbeitungslauf mit Live-Fortschritt
- Fuehrt neuen Inbound-Adapter-Subpfad batchrun/ mit Tab, Koordinator, Launcher-Port und Ergebniszeilen-Model ein; der Batch-Lauf laeuft auf einem Hintergrund-Worker, UI-Updates ausschliesslich via FX-Dispatcher. - Ergaenzt application.port.in um BatchRunProgressObserver, BatchRunCancellationToken, DocumentCompletionEvent/-Status und RunSummary; DefaultBatchRunProcessingUseCase und DocumentProcessingCoordinator melden Lauf-/Dokument-Ereignisse an den Beobachter und unterstuetzen Soft-Stop zwischen Kandidaten. - Verdrahtet BootstrapRunner so, dass die GUI den vollstaendigen Headless-Pipelinepfad (Migration, Validierung, Schema-Init, Lock, Use-Case) mit Observer und Cancellation ausfuehrt; headless-Verhalten bleibt unveraendert. - Editor-Workspace bettet den zweiten Tab ein, sperrt Tab 1 mit Hinweisbanner waehrend eines Laufs und fragt den Benutzer beim Schliessen waehrend eines laufenden Batches. - Fuegt Tests fuer Observer-Wiring, Koordinator-Lebenszyklus und Tab-Smoke-Verhalten ein; aktualisiert die GUI-Bedienanleitung und docs/betrieb.md auf den neuen Tab. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+171
-55
@@ -21,6 +21,8 @@ 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;
|
||||
import de.gecheckt.pdf.umbenenner.adapter.in.gui.GuiStartupContext;
|
||||
import de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun.GuiBatchRunLaunchOutcome;
|
||||
import de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun.GuiBatchRunLauncher;
|
||||
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.adapter.in.gui.editor.GuiConfigurationFileSnapshot;
|
||||
@@ -324,61 +326,86 @@ public class BootstrapRunner {
|
||||
this.runLockPortFactory = FilesystemRunLockPortAdapter::new;
|
||||
this.validatorFactory = StartConfigurationValidator::new;
|
||||
this.schemaInitPortFactory = SqliteSchemaInitializationAdapter::new;
|
||||
this.useCaseFactory = (startConfig, lock) -> {
|
||||
AiContentSensitivity aiContentSensitivity = resolveAiContentSensitivity(startConfig.logAiSensitive());
|
||||
RuntimeConfiguration runtimeConfig = new RuntimeConfiguration(
|
||||
startConfig.maxPages(), startConfig.maxRetriesTransient(), aiContentSensitivity);
|
||||
|
||||
AiProviderFamily activeFamily = startConfig.multiProviderConfiguration().activeProviderFamily();
|
||||
ProviderConfiguration providerConfig = startConfig.multiProviderConfiguration().activeProviderConfiguration();
|
||||
AiInvocationPort aiInvocationPort = new AiProviderSelector().select(activeFamily, providerConfig);
|
||||
|
||||
String jdbcUrl = buildJdbcUrl(startConfig);
|
||||
FingerprintPort fingerprintPort = new Sha256FingerprintAdapter();
|
||||
DocumentRecordRepository documentRecordRepository =
|
||||
new SqliteDocumentRecordRepositoryAdapter(jdbcUrl);
|
||||
ProcessingAttemptRepository processingAttemptRepository =
|
||||
new SqliteProcessingAttemptRepositoryAdapter(jdbcUrl);
|
||||
UnitOfWorkPort unitOfWorkPort =
|
||||
new SqliteUnitOfWorkAdapter(jdbcUrl);
|
||||
ProcessingLogger coordinatorLogger = new Log4jProcessingLogger(
|
||||
DocumentProcessingCoordinator.class, aiContentSensitivity);
|
||||
TargetFolderPort targetFolderPort = new FilesystemTargetFolderAdapter(startConfig.targetFolder());
|
||||
TargetFileCopyPort targetFileCopyPort = new FilesystemTargetFileCopyAdapter(startConfig.targetFolder());
|
||||
DocumentProcessingCoordinator documentProcessingCoordinator =
|
||||
new DocumentProcessingCoordinator(documentRecordRepository, processingAttemptRepository,
|
||||
unitOfWorkPort, targetFolderPort, targetFileCopyPort, coordinatorLogger,
|
||||
startConfig.maxRetriesTransient(),
|
||||
startConfig.maxTitleLength(),
|
||||
activeFamily.getIdentifier());
|
||||
|
||||
PromptPort promptPort = new FilesystemPromptPortAdapter(startConfig.promptTemplateFile());
|
||||
ClockPort clockPort = new SystemClockAdapter();
|
||||
AiResponseValidator aiResponseValidator = new AiResponseValidator(clockPort, startConfig.maxTitleLength());
|
||||
AiNamingService aiNamingService = new AiNamingService(
|
||||
aiInvocationPort,
|
||||
promptPort,
|
||||
aiResponseValidator,
|
||||
providerConfig.model(),
|
||||
startConfig.maxTextCharacters(),
|
||||
startConfig.maxTitleLength());
|
||||
|
||||
ProcessingLogger useCaseLogger = new Log4jProcessingLogger(
|
||||
DefaultBatchRunProcessingUseCase.class, aiContentSensitivity);
|
||||
return new DefaultBatchRunProcessingUseCase(
|
||||
runtimeConfig,
|
||||
lock,
|
||||
new SourceDocumentCandidatesPortAdapter(startConfig.sourceFolder()),
|
||||
new PdfTextExtractionPortAdapter(),
|
||||
fingerprintPort,
|
||||
documentProcessingCoordinator,
|
||||
aiNamingService,
|
||||
useCaseLogger);
|
||||
};
|
||||
this.useCaseFactory = (startConfig, lock) -> buildProductionBatchUseCase(
|
||||
startConfig, lock,
|
||||
de.gecheckt.pdf.umbenenner.application.port.in.BatchRunProgressObserver.noOp(),
|
||||
de.gecheckt.pdf.umbenenner.application.port.in.BatchRunCancellationToken.neverCancelled());
|
||||
this.commandFactory = SchedulerBatchCommand::new;
|
||||
this.guiAdapterFactory = GuiAdapter::new;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wires the production batch-processing use case with the supplied progress observer and
|
||||
* cancellation token.
|
||||
* <p>
|
||||
* Shared wiring for the headless path (where observer and token default to no-ops) and the
|
||||
* GUI processing-run path (where both carry live callbacks from the UI). The method is
|
||||
* intentionally factored out so a single wiring description serves both entry points.
|
||||
*
|
||||
* @param startConfig validated startup configuration; must not be null
|
||||
* @param runLockPort acquired run-lock port; must not be null
|
||||
* @param progressObserver observer forwarded into the use case; must not be null
|
||||
* @param cancellationToken cancellation token forwarded into the use case; must not be null
|
||||
* @return a fully wired production batch use case; never null
|
||||
*/
|
||||
private BatchRunProcessingUseCase buildProductionBatchUseCase(
|
||||
StartConfiguration startConfig,
|
||||
RunLockPort runLockPort,
|
||||
de.gecheckt.pdf.umbenenner.application.port.in.BatchRunProgressObserver progressObserver,
|
||||
de.gecheckt.pdf.umbenenner.application.port.in.BatchRunCancellationToken cancellationToken) {
|
||||
AiContentSensitivity aiContentSensitivity = resolveAiContentSensitivity(startConfig.logAiSensitive());
|
||||
RuntimeConfiguration runtimeConfig = new RuntimeConfiguration(
|
||||
startConfig.maxPages(), startConfig.maxRetriesTransient(), aiContentSensitivity);
|
||||
|
||||
AiProviderFamily activeFamily = startConfig.multiProviderConfiguration().activeProviderFamily();
|
||||
ProviderConfiguration providerConfig = startConfig.multiProviderConfiguration().activeProviderConfiguration();
|
||||
AiInvocationPort aiInvocationPort = new AiProviderSelector().select(activeFamily, providerConfig);
|
||||
|
||||
String jdbcUrl = buildJdbcUrl(startConfig);
|
||||
FingerprintPort fingerprintPort = new Sha256FingerprintAdapter();
|
||||
DocumentRecordRepository documentRecordRepository =
|
||||
new SqliteDocumentRecordRepositoryAdapter(jdbcUrl);
|
||||
ProcessingAttemptRepository processingAttemptRepository =
|
||||
new SqliteProcessingAttemptRepositoryAdapter(jdbcUrl);
|
||||
UnitOfWorkPort unitOfWorkPort =
|
||||
new SqliteUnitOfWorkAdapter(jdbcUrl);
|
||||
ProcessingLogger coordinatorLogger = new Log4jProcessingLogger(
|
||||
DocumentProcessingCoordinator.class, aiContentSensitivity);
|
||||
TargetFolderPort targetFolderPort = new FilesystemTargetFolderAdapter(startConfig.targetFolder());
|
||||
TargetFileCopyPort targetFileCopyPort = new FilesystemTargetFileCopyAdapter(startConfig.targetFolder());
|
||||
DocumentProcessingCoordinator documentProcessingCoordinator =
|
||||
new DocumentProcessingCoordinator(documentRecordRepository, processingAttemptRepository,
|
||||
unitOfWorkPort, targetFolderPort, targetFileCopyPort, coordinatorLogger,
|
||||
startConfig.maxRetriesTransient(),
|
||||
startConfig.maxTitleLength(),
|
||||
activeFamily.getIdentifier());
|
||||
|
||||
PromptPort promptPort = new FilesystemPromptPortAdapter(startConfig.promptTemplateFile());
|
||||
ClockPort clockPort = new SystemClockAdapter();
|
||||
AiResponseValidator aiResponseValidator = new AiResponseValidator(clockPort, startConfig.maxTitleLength());
|
||||
AiNamingService aiNamingService = new AiNamingService(
|
||||
aiInvocationPort,
|
||||
promptPort,
|
||||
aiResponseValidator,
|
||||
providerConfig.model(),
|
||||
startConfig.maxTextCharacters(),
|
||||
startConfig.maxTitleLength());
|
||||
|
||||
ProcessingLogger useCaseLogger = new Log4jProcessingLogger(
|
||||
DefaultBatchRunProcessingUseCase.class, aiContentSensitivity);
|
||||
return new DefaultBatchRunProcessingUseCase(
|
||||
runtimeConfig,
|
||||
runLockPort,
|
||||
new SourceDocumentCandidatesPortAdapter(startConfig.sourceFolder()),
|
||||
new PdfTextExtractionPortAdapter(),
|
||||
fingerprintPort,
|
||||
documentProcessingCoordinator,
|
||||
aiNamingService,
|
||||
useCaseLogger,
|
||||
progressObserver,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the BootstrapRunner with custom factories for testing, without a migration step.
|
||||
* <p>
|
||||
@@ -639,6 +666,7 @@ public class BootstrapRunner {
|
||||
de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionExecutionService correctionExecutionService =
|
||||
new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionExecutionService(
|
||||
new de.gecheckt.pdf.umbenenner.adapter.out.resourcecreation.FilesystemResourceCreationAdapter());
|
||||
GuiBatchRunLauncher batchRunLauncher = this::launchGuiBatchRun;
|
||||
|
||||
if (configPathOverride.isEmpty()) {
|
||||
return new GuiStartupContext(
|
||||
@@ -651,7 +679,8 @@ public class BootstrapRunner {
|
||||
providerTechnicalTestService,
|
||||
pathCheckPort,
|
||||
technicalTestOrchestrator,
|
||||
correctionExecutionService);
|
||||
correctionExecutionService,
|
||||
batchRunLauncher);
|
||||
}
|
||||
|
||||
Path configPath = Paths.get(configPathOverride.get());
|
||||
@@ -669,7 +698,8 @@ public class BootstrapRunner {
|
||||
providerTechnicalTestService,
|
||||
pathCheckPort,
|
||||
technicalTestOrchestrator,
|
||||
correctionExecutionService);
|
||||
correctionExecutionService,
|
||||
batchRunLauncher);
|
||||
}
|
||||
|
||||
LOG.info("GUI startup: configuration file confirmed at: {}", configPath.toAbsolutePath());
|
||||
@@ -677,7 +707,7 @@ public class BootstrapRunner {
|
||||
GuiConfigurationEditorState loadedState = loadGuiConfigurationState(configPath);
|
||||
return new GuiStartupContext(loadedState, Optional.empty(), loader, writer,
|
||||
modelCatalogPort, apiKeyResolutionPort, providerTechnicalTestService, pathCheckPort,
|
||||
technicalTestOrchestrator, correctionExecutionService);
|
||||
technicalTestOrchestrator, correctionExecutionService, batchRunLauncher);
|
||||
} catch (GuiConfigurationLoadException e) {
|
||||
LOG.error("GUI startup: configuration could not be loaded, starting without it: {}",
|
||||
e.getMessage(), e);
|
||||
@@ -691,10 +721,96 @@ public class BootstrapRunner {
|
||||
providerTechnicalTestService,
|
||||
pathCheckPort,
|
||||
technicalTestOrchestrator,
|
||||
correctionExecutionService);
|
||||
correctionExecutionService,
|
||||
batchRunLauncher);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes exactly one batch run triggered by the GUI's processing-run tab.
|
||||
* <p>
|
||||
* Mirrors the headless bootstrap pipeline: legacy migration, configuration loading and
|
||||
* validation, SQLite schema initialisation, run-lock acquisition, use-case wiring, and
|
||||
* execution. Forwards the supplied observer and cancellation token into the wired use
|
||||
* case so the GUI receives live progress callbacks and can request a soft-stop.
|
||||
* <p>
|
||||
* All known hard startup failures are mapped to
|
||||
* {@link GuiBatchRunLaunchOutcome#rejected(String)}. Unexpected runtime exceptions
|
||||
* that escape after startup are mapped to
|
||||
* {@link GuiBatchRunLaunchOutcome#failedAfterStart(String)} so the GUI can reach the
|
||||
* defined terminal state instead of becoming stuck.
|
||||
*
|
||||
* @param configFilePath path to the {@code .properties} configuration; must exist on disk
|
||||
* @param progressObserver observer forwarded into the use case; must not be null
|
||||
* @param cancellationToken token forwarded into the use case; must not be null
|
||||
* @return the outcome for the run; never null
|
||||
*/
|
||||
GuiBatchRunLaunchOutcome launchGuiBatchRun(
|
||||
Path configFilePath,
|
||||
de.gecheckt.pdf.umbenenner.application.port.in.BatchRunProgressObserver progressObserver,
|
||||
de.gecheckt.pdf.umbenenner.application.port.in.BatchRunCancellationToken cancellationToken) {
|
||||
Objects.requireNonNull(configFilePath, "configFilePath must not be null");
|
||||
Objects.requireNonNull(progressObserver, "progressObserver must not be null");
|
||||
Objects.requireNonNull(cancellationToken, "cancellationToken must not be null");
|
||||
LOG.info("GUI-Verarbeitungslauf: Startanforderung für Konfiguration {}.", configFilePath);
|
||||
try {
|
||||
if (!Files.exists(configFilePath)) {
|
||||
return GuiBatchRunLaunchOutcome.rejected(
|
||||
"Konfigurationsdatei wurde nicht gefunden: " + configFilePath);
|
||||
}
|
||||
migrateConfigurationIfNeeded(configFilePath);
|
||||
StartConfiguration config = loadAndValidateConfiguration(configFilePath);
|
||||
initializeSchema(config);
|
||||
RunLockPort runLockPort = runLockPortFactory.create(resolveLockFilePath(config));
|
||||
BatchRunContext runContext = createRunContext();
|
||||
BatchRunProcessingUseCase useCase = buildProductionBatchUseCase(
|
||||
config, runLockPort, progressObserver, cancellationToken);
|
||||
BatchRunOutcome outcome = useCase.execute(runContext);
|
||||
runContext.setEndInstant(Instant.now());
|
||||
return mapGuiRunOutcome(outcome, runContext);
|
||||
} catch (ConfigurationLoadingException e) {
|
||||
LOG.error("GUI-Verarbeitungslauf: Konfiguration konnte nicht geladen werden: {}",
|
||||
e.getMessage(), e);
|
||||
return GuiBatchRunLaunchOutcome.rejected(
|
||||
"Konfiguration konnte nicht geladen werden: " + e.getMessage());
|
||||
} catch (InvalidStartConfigurationException e) {
|
||||
LOG.error("GUI-Verarbeitungslauf: Konfiguration ist nicht lauffähig: {}", e.getMessage());
|
||||
return GuiBatchRunLaunchOutcome.rejected(
|
||||
"Die gespeicherte Konfiguration ist nicht lauffähig: " + e.getMessage());
|
||||
} catch (DocumentPersistenceException e) {
|
||||
LOG.error("GUI-Verarbeitungslauf: SQLite-Initialisierung fehlgeschlagen: {}",
|
||||
e.getMessage(), e);
|
||||
return GuiBatchRunLaunchOutcome.rejected(
|
||||
"SQLite-Datenbank konnte nicht vorbereitet werden: " + e.getMessage());
|
||||
} catch (RuntimeException e) {
|
||||
LOG.error("GUI-Verarbeitungslauf: Unerwarteter Fehler: {}", e.getMessage(), e);
|
||||
return GuiBatchRunLaunchOutcome.failedAfterStart(
|
||||
"Unerwarteter Fehler im Verarbeitungslauf: "
|
||||
+ (e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
private GuiBatchRunLaunchOutcome mapGuiRunOutcome(BatchRunOutcome outcome, BatchRunContext runContext) {
|
||||
return switch (outcome) {
|
||||
case SUCCESS -> {
|
||||
LOG.info("GUI-Verarbeitungslauf: Lauf beendet. RunId: {}", runContext.runId());
|
||||
yield GuiBatchRunLaunchOutcome.completed();
|
||||
}
|
||||
case LOCK_UNAVAILABLE -> {
|
||||
LOG.warn("GUI-Verarbeitungslauf: Laufsperre bereits vergeben. RunId: {}",
|
||||
runContext.runId());
|
||||
yield GuiBatchRunLaunchOutcome.rejected(
|
||||
"Ein anderer Verarbeitungslauf hält bereits die Laufsperre.");
|
||||
}
|
||||
case FAILURE -> {
|
||||
LOG.error("GUI-Verarbeitungslauf: Lauf mit Fehler beendet. RunId: {}",
|
||||
runContext.runId());
|
||||
yield GuiBatchRunLaunchOutcome.failedAfterStart(
|
||||
"Der Verarbeitungslauf konnte nicht erfolgreich abgeschlossen werden.");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the {@link AiModelCatalogPort} dispatcher for use in the GUI startup context.
|
||||
* <p>
|
||||
|
||||
Reference in New Issue
Block a user