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:
2026-04-23 12:04:22 +02:00
parent f4a1bce9ae
commit 9fd5bd5a52
40 changed files with 3478 additions and 223 deletions
@@ -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 -> {