V2.8: Selektive Wiederverarbeitung und Statusreset in der GUI
- Mehrfachauswahl mit CheckBox-Spalte und Master-Tri-State-Checkbox - Gezielter Mini-Lauf über ausgewählte Einträge (unabhängig vom Status) - Statusreset für ausgewählte Einträge (Stammsatz + Versuchshistorie) - Fehlende Quelldatei im Mini-Lauf wird als FAILED_PERMANENT synthetisiert - Identische Zieldatei wird als SUCCESS ohne erneute KI-Verarbeitung erkannt - Weiche Stop-Semantik erhält zurückgesetzte Einträge unverändert - Nicht-ausgewählte Einträge bleiben in allen Pfaden unberührt - Buttons reagieren jetzt korrekt auf Auswahländerungen Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+190
-4
@@ -10,6 +10,7 @@ import java.time.Instant;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Properties;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
@@ -23,6 +24,8 @@ 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.batchrun.GuiMiniRunLauncher;
|
||||
import de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun.GuiResetDocumentStatusPort;
|
||||
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;
|
||||
@@ -52,6 +55,8 @@ import de.gecheckt.pdf.umbenenner.application.config.provider.ProviderConfigurat
|
||||
import de.gecheckt.pdf.umbenenner.application.config.startup.StartConfiguration;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.in.BatchRunOutcome;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.in.BatchRunProcessingUseCase;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.in.ResetDocumentStatusResult;
|
||||
import de.gecheckt.pdf.umbenenner.application.usecase.DefaultResetDocumentStatusUseCase;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.AiContentSensitivity;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.AiInvocationPort;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.ClockPort;
|
||||
@@ -81,6 +86,7 @@ import de.gecheckt.pdf.umbenenner.bootstrap.adapter.Log4jProcessingLogger;
|
||||
import de.gecheckt.pdf.umbenenner.bootstrap.startup.StartupArguments;
|
||||
import de.gecheckt.pdf.umbenenner.bootstrap.startup.StartupMode;
|
||||
import de.gecheckt.pdf.umbenenner.domain.model.BatchRunContext;
|
||||
import de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint;
|
||||
import de.gecheckt.pdf.umbenenner.domain.model.RunId;
|
||||
|
||||
/**
|
||||
@@ -667,6 +673,10 @@ public class BootstrapRunner {
|
||||
new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionExecutionService(
|
||||
new de.gecheckt.pdf.umbenenner.adapter.out.resourcecreation.FilesystemResourceCreationAdapter());
|
||||
GuiBatchRunLauncher batchRunLauncher = this::launchGuiBatchRun;
|
||||
de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun.GuiMiniRunLauncher miniRunLauncher =
|
||||
this::launchGuiMiniBatchRun;
|
||||
de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun.GuiResetDocumentStatusPort resetPort =
|
||||
this::resetDocumentStatusForGui;
|
||||
|
||||
if (configPathOverride.isEmpty()) {
|
||||
return new GuiStartupContext(
|
||||
@@ -680,7 +690,9 @@ public class BootstrapRunner {
|
||||
pathCheckPort,
|
||||
technicalTestOrchestrator,
|
||||
correctionExecutionService,
|
||||
batchRunLauncher);
|
||||
batchRunLauncher,
|
||||
miniRunLauncher,
|
||||
resetPort);
|
||||
}
|
||||
|
||||
Path configPath = Paths.get(configPathOverride.get());
|
||||
@@ -699,7 +711,9 @@ public class BootstrapRunner {
|
||||
pathCheckPort,
|
||||
technicalTestOrchestrator,
|
||||
correctionExecutionService,
|
||||
batchRunLauncher);
|
||||
batchRunLauncher,
|
||||
miniRunLauncher,
|
||||
resetPort);
|
||||
}
|
||||
|
||||
LOG.info("GUI startup: configuration file confirmed at: {}", configPath.toAbsolutePath());
|
||||
@@ -707,7 +721,8 @@ public class BootstrapRunner {
|
||||
GuiConfigurationEditorState loadedState = loadGuiConfigurationState(configPath);
|
||||
return new GuiStartupContext(loadedState, Optional.empty(), loader, writer,
|
||||
modelCatalogPort, apiKeyResolutionPort, providerTechnicalTestService, pathCheckPort,
|
||||
technicalTestOrchestrator, correctionExecutionService, batchRunLauncher);
|
||||
technicalTestOrchestrator, correctionExecutionService, batchRunLauncher,
|
||||
miniRunLauncher, resetPort);
|
||||
} catch (GuiConfigurationLoadException e) {
|
||||
LOG.error("GUI startup: configuration could not be loaded, starting without it: {}",
|
||||
e.getMessage(), e);
|
||||
@@ -722,7 +737,9 @@ public class BootstrapRunner {
|
||||
pathCheckPort,
|
||||
technicalTestOrchestrator,
|
||||
correctionExecutionService,
|
||||
batchRunLauncher);
|
||||
batchRunLauncher,
|
||||
miniRunLauncher,
|
||||
resetPort);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -790,6 +807,175 @@ public class BootstrapRunner {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a targeted mini batch run restricted to the supplied fingerprint set,
|
||||
* triggered by the GUI's "Erneut verarbeiten" action.
|
||||
* <p>
|
||||
* Mirrors the full headless bootstrap pipeline (legacy migration, configuration loading
|
||||
* and validation, SQLite schema initialisation, run-lock acquisition, use-case wiring,
|
||||
* and execution) but builds a {@link BatchRunContext} with a fingerprint filter so only
|
||||
* the specified documents are processed.
|
||||
* <p>
|
||||
* The run lock is acquired to prevent racing with a concurrent headless run. If the lock
|
||||
* is unavailable, all fingerprints are returned as failures with a German message.
|
||||
*
|
||||
* @param configFilePath path to the {@code .properties} configuration; must exist on disk
|
||||
* @param fingerprintFilter the set of document fingerprints to process; must not be null
|
||||
* @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 mini-run; never null
|
||||
*/
|
||||
GuiBatchRunLaunchOutcome launchGuiMiniBatchRun(
|
||||
Path configFilePath,
|
||||
Set<DocumentFingerprint> fingerprintFilter,
|
||||
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(fingerprintFilter, "fingerprintFilter must not be null");
|
||||
Objects.requireNonNull(progressObserver, "progressObserver must not be null");
|
||||
Objects.requireNonNull(cancellationToken, "cancellationToken must not be null");
|
||||
LOG.info("GUI-Mini-Verarbeitungslauf: Startanforderung für {} Dokument(e), Konfiguration {}.",
|
||||
fingerprintFilter.size(), 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();
|
||||
// Apply the fingerprint filter to restrict processing to the selected documents
|
||||
BatchRunContext filteredContext = runContext.withFingerprintFilter(fingerprintFilter);
|
||||
BatchRunProcessingUseCase useCase = buildProductionBatchUseCase(
|
||||
config, runLockPort, progressObserver, cancellationToken);
|
||||
BatchRunOutcome outcome = useCase.execute(filteredContext);
|
||||
filteredContext.setEndInstant(Instant.now());
|
||||
return mapGuiRunOutcome(outcome, filteredContext);
|
||||
} catch (ConfigurationLoadingException e) {
|
||||
LOG.error("GUI-Mini-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-Mini-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-Mini-Verarbeitungslauf: SQLite-Initialisierung fehlgeschlagen: {}",
|
||||
e.getMessage(), e);
|
||||
return GuiBatchRunLaunchOutcome.rejected(
|
||||
"SQLite-Datenbank konnte nicht vorbereitet werden: " + e.getMessage());
|
||||
} catch (RuntimeException e) {
|
||||
LOG.error("GUI-Mini-Verarbeitungslauf: Unerwarteter Fehler: {}", e.getMessage(), e);
|
||||
return GuiBatchRunLaunchOutcome.failedAfterStart(
|
||||
"Unerwarteter Fehler im Mini-Verarbeitungslauf: "
|
||||
+ (e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the processing status of the specified documents by deleting all persistence
|
||||
* data for their fingerprints, triggered by the GUI's "Status zurücksetzen" action.
|
||||
* <p>
|
||||
* This method initialises the SQLite schema (if needed) and runs
|
||||
* {@link DefaultResetDocumentStatusUseCase} with best-effort semantics. The run lock is
|
||||
* acquired to avoid racing with a concurrent headless run. If the lock is unavailable,
|
||||
* all fingerprints are returned as failures with a German message.
|
||||
* <p>
|
||||
* Configuration migration and validation are applied before the reset executes to ensure
|
||||
* the database path is resolvable.
|
||||
*
|
||||
* @param configFilePath path to the {@code .properties} configuration; must exist on disk
|
||||
* @param fingerprints the set of document fingerprints to reset; must not be null
|
||||
* @return the result of the reset operation; never null
|
||||
*/
|
||||
ResetDocumentStatusResult resetDocumentStatusForGui(
|
||||
Path configFilePath,
|
||||
Set<DocumentFingerprint> fingerprints) {
|
||||
Objects.requireNonNull(configFilePath, "configFilePath must not be null");
|
||||
Objects.requireNonNull(fingerprints, "fingerprints must not be null");
|
||||
LOG.info("GUI-Status-Reset: {} Dokument(e) zurücksetzen, Konfiguration {}.",
|
||||
fingerprints.size(), configFilePath);
|
||||
|
||||
if (!Files.exists(configFilePath)) {
|
||||
String msg = "Konfigurationsdatei nicht gefunden: " + configFilePath;
|
||||
LOG.error("GUI-Status-Reset: {}", msg);
|
||||
return allFailures(fingerprints, msg);
|
||||
}
|
||||
|
||||
try {
|
||||
migrateConfigurationIfNeeded(configFilePath);
|
||||
StartConfiguration config = loadAndValidateConfiguration(configFilePath);
|
||||
initializeSchema(config);
|
||||
|
||||
String jdbcUrl = buildJdbcUrl(config);
|
||||
UnitOfWorkPort unitOfWorkPort = new SqliteUnitOfWorkAdapter(jdbcUrl);
|
||||
ProcessingLogger resetLogger = new Log4jProcessingLogger(
|
||||
DefaultResetDocumentStatusUseCase.class,
|
||||
resolveAiContentSensitivity(config.logAiSensitive()));
|
||||
|
||||
RunLockPort runLockPort = runLockPortFactory.create(resolveLockFilePath(config));
|
||||
try {
|
||||
runLockPort.acquire();
|
||||
} catch (de.gecheckt.pdf.umbenenner.application.port.out.RunLockUnavailableException e) {
|
||||
LOG.warn("GUI-Status-Reset: Laufsperre nicht verfügbar — Reset abgelehnt.");
|
||||
return allFailures(fingerprints, "Lauf blockiert — Lock nicht verfügbar");
|
||||
}
|
||||
|
||||
try {
|
||||
DefaultResetDocumentStatusUseCase useCase =
|
||||
new DefaultResetDocumentStatusUseCase(unitOfWorkPort, resetLogger);
|
||||
ResetDocumentStatusResult result = useCase.reset(fingerprints);
|
||||
LOG.info("GUI-Status-Reset abgeschlossen: {} erfolgreich, {} fehlgeschlagen.",
|
||||
result.successCount(), result.failureCount());
|
||||
return result;
|
||||
} finally {
|
||||
try {
|
||||
runLockPort.release();
|
||||
} catch (RuntimeException e) {
|
||||
LOG.warn("GUI-Status-Reset: Laufsperre konnte nicht freigegeben werden: {}",
|
||||
e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (ConfigurationLoadingException e) {
|
||||
LOG.error("GUI-Status-Reset: Konfiguration konnte nicht geladen werden: {}", e.getMessage(), e);
|
||||
return allFailures(fingerprints, "Konfiguration konnte nicht geladen werden: " + e.getMessage());
|
||||
} catch (InvalidStartConfigurationException e) {
|
||||
LOG.error("GUI-Status-Reset: Konfiguration ist nicht lauffähig: {}", e.getMessage());
|
||||
return allFailures(fingerprints, "Die Konfiguration ist nicht lauffähig: " + e.getMessage());
|
||||
} catch (DocumentPersistenceException e) {
|
||||
LOG.error("GUI-Status-Reset: SQLite-Initialisierung fehlgeschlagen: {}", e.getMessage(), e);
|
||||
return allFailures(fingerprints, "SQLite-Datenbank konnte nicht vorbereitet werden: " + e.getMessage());
|
||||
} catch (RuntimeException e) {
|
||||
LOG.error("GUI-Status-Reset: Unerwarteter Fehler: {}", e.getMessage(), e);
|
||||
return allFailures(fingerprints, "Unerwarteter Fehler: "
|
||||
+ (e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a {@link ResetDocumentStatusResult} where every requested fingerprint is
|
||||
* recorded as a failure with the given error message.
|
||||
* <p>
|
||||
* Used to produce a consistently structured failure result when a hard prerequisite
|
||||
* (configuration load, lock acquisition) prevents any individual reset from running.
|
||||
*
|
||||
* @param fingerprints the full set of requested fingerprints; must not be null
|
||||
* @param errorMessage the German error message to attach to every failure entry
|
||||
* @return a result with all fingerprints in the failure map; never null
|
||||
*/
|
||||
private static ResetDocumentStatusResult allFailures(
|
||||
Set<DocumentFingerprint> fingerprints, String errorMessage) {
|
||||
java.util.Map<DocumentFingerprint, String> failures = new java.util.HashMap<>();
|
||||
for (DocumentFingerprint fp : fingerprints) {
|
||||
failures.put(fp, errorMessage);
|
||||
}
|
||||
return new ResetDocumentStatusResult(fingerprints.size(), java.util.Set.of(), failures);
|
||||
}
|
||||
|
||||
private GuiBatchRunLaunchOutcome mapGuiRunOutcome(BatchRunOutcome outcome, BatchRunContext runContext) {
|
||||
return switch (outcome) {
|
||||
case SUCCESS -> {
|
||||
|
||||
Reference in New Issue
Block a user