Fix #41: Historischen KI-Dateinamen für übersprungene Dokumente in Ergebnistabelle anzeigen

Neue Komponenten:
- ResolveHistoricalFileNameUseCase (port/in) und DefaultResolveHistoricalFileNameUseCase (usecase)
- GuiHistoricalFileNamePort (GUI-interner Port, folgt dem Muster von GuiManualFileRenamePort)

GuiBatchRunCoordinator ruft in toRow() für SKIPPED-Zeilen ohne finalName den
historicalFileNamePort auf und trägt den Rückgabewert als neuen Dateinamen ein.

Bootstrap verdrahtet resolveHistoricalFileNameForGui als GuiHistoricalFileNamePort
und übergibt ihn über GuiStartupContext an den GUI-Adapter.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-27 10:54:31 +02:00
parent 385bda5331
commit 1db6e27be8
10 changed files with 490 additions and 25 deletions
@@ -24,6 +24,7 @@ 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.GuiHistoricalFileNamePort;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun.GuiManualFileRenamePort;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiConfigurationEditorState;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiConfigurationEditorStateFactory;
@@ -58,6 +59,8 @@ import de.gecheckt.pdf.umbenenner.application.port.in.BatchRunProcessingUseCase;
import de.gecheckt.pdf.umbenenner.application.port.in.ManualFileRenameRequest;
import de.gecheckt.pdf.umbenenner.application.port.in.ManualFileRenameResult;
import de.gecheckt.pdf.umbenenner.application.port.in.ManualFileRenameUseCase;
import de.gecheckt.pdf.umbenenner.application.port.in.ResolveHistoricalFileNameUseCase;
import de.gecheckt.pdf.umbenenner.application.usecase.DefaultResolveHistoricalFileNameUseCase;
import de.gecheckt.pdf.umbenenner.application.port.in.ResetDocumentStatusResult;
import de.gecheckt.pdf.umbenenner.application.port.out.AiContentSensitivity;
import de.gecheckt.pdf.umbenenner.application.port.out.AiInvocationPort;
@@ -683,6 +686,7 @@ public class BootstrapRunner {
de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun.GuiResetDocumentStatusPort resetPort =
this::resetDocumentStatusForGui;
GuiManualFileRenamePort manualRenamePort = this::performGuiManualFileRename;
GuiHistoricalFileNamePort historicalFileNamePort = this::resolveHistoricalFileNameForGui;
if (configPathOverride.isEmpty()) {
return new GuiStartupContext(
@@ -699,7 +703,8 @@ public class BootstrapRunner {
batchRunLauncher,
miniRunLauncher,
resetPort,
manualRenamePort);
manualRenamePort,
historicalFileNamePort);
}
Path configPath = Paths.get(configPathOverride.get());
@@ -721,7 +726,8 @@ public class BootstrapRunner {
batchRunLauncher,
miniRunLauncher,
resetPort,
manualRenamePort);
manualRenamePort,
historicalFileNamePort);
}
LOG.info("GUI startup: configuration file confirmed at: {}", configPath.toAbsolutePath());
@@ -730,7 +736,7 @@ public class BootstrapRunner {
return new GuiStartupContext(loadedState, Optional.empty(), loader, writer,
modelCatalogPort, apiKeyResolutionPort, providerTechnicalTestService, pathCheckPort,
technicalTestOrchestrator, correctionExecutionService, batchRunLauncher,
miniRunLauncher, resetPort, manualRenamePort);
miniRunLauncher, resetPort, manualRenamePort, historicalFileNamePort);
} catch (GuiConfigurationLoadException e) {
LOG.error("GUI startup: configuration could not be loaded, starting without it: {}",
e.getMessage(), e);
@@ -748,7 +754,8 @@ public class BootstrapRunner {
batchRunLauncher,
miniRunLauncher,
resetPort,
manualRenamePort);
manualRenamePort,
historicalFileNamePort);
}
}
@@ -1062,6 +1069,49 @@ public class BootstrapRunner {
}
}
/**
* Resolves the historical AI-proposed target filename for a document identified by
* {@code fingerprint}, using the configuration at {@code configFilePath}.
* <p>
* Loads the configuration, initialises the schema and delegates to
* {@link ResolveHistoricalFileNameUseCase}. Technical errors during loading or querying
* are caught and returned as an empty {@link Optional}; they are never propagated to the
* caller.
* <p>
* Runs on the GUI worker thread. Blocking I/O is therefore acceptable.
*
* @param configFilePath path to the active {@code .properties} file; must not be {@code null}
* @param fingerprint content-based document identity; must not be {@code null}
* @return the last successfully written target filename, or empty if not available
*/
Optional<String> resolveHistoricalFileNameForGui(
Path configFilePath,
DocumentFingerprint fingerprint) {
Objects.requireNonNull(configFilePath, "configFilePath must not be null");
Objects.requireNonNull(fingerprint, "fingerprint must not be null");
if (!Files.exists(configFilePath)) {
LOG.debug("Historischer Dateiname: Konfigurationsdatei nicht gefunden: {}", configFilePath);
return Optional.empty();
}
try {
migrateConfigurationIfNeeded(configFilePath);
StartConfiguration config = loadAndValidateConfiguration(configFilePath);
initializeSchema(config);
String jdbcUrl = buildJdbcUrl(config);
DocumentRecordRepository documentRecordRepository =
new SqliteDocumentRecordRepositoryAdapter(jdbcUrl);
ResolveHistoricalFileNameUseCase useCase =
new DefaultResolveHistoricalFileNameUseCase(documentRecordRepository);
return useCase.resolveHistoricalFileName(fingerprint);
} catch (Exception e) {
LOG.debug("Historischer Dateiname konnte nicht abgefragt werden für {}: {}",
fingerprint.sha256Hex(), e.getMessage());
return Optional.empty();
}
}
/**
* Builds a {@link ResetDocumentStatusResult} where every requested fingerprint is
* recorded as a failure with the given error message.