diff --git a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiConfigurationEditorWorkspace.java b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiConfigurationEditorWorkspace.java
index 1e5da5b..9b5b432 100644
--- a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiConfigurationEditorWorkspace.java
+++ b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiConfigurationEditorWorkspace.java
@@ -17,7 +17,7 @@ import org.apache.logging.log4j.Logger;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun.GuiBatchRunLauncher;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun.GuiBatchRunTab;
-import de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun.GuiHistoricalFileNamePort;
+import de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun.GuiHistoricalDocumentContextPort;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun.GuiManualFileRenamePort;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun.GuiMiniRunLauncher;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun.GuiResetDocumentStatusPort;
@@ -373,10 +373,10 @@ public final class GuiConfigurationEditorWorkspace {
private final GuiManualFileRenamePort manualFileRenamePort;
/**
- * Port used by the processing-run coordinator to resolve the historical AI-proposed
- * filename for skipped documents. Supplied by Bootstrap via the startup context.
+ * Port used by the processing-run coordinator to resolve the historical processing context
+ * for skipped documents. Supplied by Bootstrap via the startup context.
*/
- private final GuiHistoricalFileNamePort historicalFileNamePort;
+ private final GuiHistoricalDocumentContextPort historicalDocumentContextPort;
/**
* Second main tab of the window that drives the live processing-run view. Created
@@ -453,7 +453,7 @@ public final class GuiConfigurationEditorWorkspace {
this.miniRunLauncher = effectiveContext.miniRunLauncher();
this.resetDocumentStatusPort = effectiveContext.resetDocumentStatusPort();
this.manualFileRenamePort = effectiveContext.manualFileRenamePort();
- this.historicalFileNamePort = effectiveContext.historicalFileNamePort();
+ this.historicalDocumentContextPort = effectiveContext.historicalDocumentContextPort();
this.batchRunTab = new GuiBatchRunTab(
() -> this.batchRunLauncher,
() -> this.miniRunLauncher,
@@ -462,7 +462,7 @@ public final class GuiConfigurationEditorWorkspace {
this::isSavedConfigurationReady,
this::applyBatchRunLockState,
() -> this.manualFileRenamePort,
- () -> this.historicalFileNamePort,
+ () -> this.historicalDocumentContextPort,
this::editorSourceFolder,
this::editorTargetFolder);
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 5e47a3d..6872d68 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
@@ -6,7 +6,7 @@ import java.util.Set;
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.GuiHistoricalDocumentContextPort;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun.GuiManualFileRenamePort;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun.GuiMiniRunLauncher;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun.GuiResetDocumentStatusPort;
@@ -42,8 +42,8 @@ import de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint;
* mini-runs for selected documents, the {@link GuiResetDocumentStatusPort} used to
* reset the persistence status of selected documents, and the
* {@link GuiManualFileRenamePort} used to manually rename a target file from the GUI, and
- * the {@link GuiHistoricalFileNamePort} used to retrieve the historical AI-proposed filename
- * for documents that were skipped in the current run.
+ * the {@link GuiHistoricalDocumentContextPort} used to retrieve the historical processing
+ * context for documents that were skipped in the current run.
*
* 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.
@@ -63,7 +63,7 @@ public record GuiStartupContext(
GuiMiniRunLauncher miniRunLauncher,
GuiResetDocumentStatusPort resetDocumentStatusPort,
GuiManualFileRenamePort manualFileRenamePort,
- GuiHistoricalFileNamePort historicalFileNamePort) {
+ GuiHistoricalDocumentContextPort historicalDocumentContextPort) {
/**
* Creates a fully wired startup context.
@@ -85,8 +85,8 @@ public record GuiStartupContext(
* documents; must not be {@code null}
* @param manualFileRenamePort bridge that renames a target file manually from the GUI;
* must not be {@code null}
- * @param historicalFileNamePort bridge that resolves the historical AI-proposed filename
- * for skipped documents; must not be {@code null}
+ * @param historicalDocumentContextPort bridge that resolves the historical processing context
+ * for skipped documents; must not be {@code null}
*/
public GuiStartupContext {
initialState = Objects.requireNonNull(initialState, "initialState must not be null");
@@ -115,8 +115,8 @@ public record GuiStartupContext(
"resetDocumentStatusPort must not be null");
manualFileRenamePort = Objects.requireNonNull(manualFileRenamePort,
"manualFileRenamePort must not be null");
- historicalFileNamePort = Objects.requireNonNull(historicalFileNamePort,
- "historicalFileNamePort must not be null");
+ historicalDocumentContextPort = Objects.requireNonNull(historicalDocumentContextPort,
+ "historicalDocumentContextPort must not be null");
}
/**
@@ -157,7 +157,7 @@ public record GuiStartupContext(
modelCatalogPort, apiKeyResolutionPort, providerTechnicalTestService, pathCheckPort,
technicalTestOrchestrator, correctionExecutionService, batchRunLauncher,
miniRunLauncher, resetDocumentStatusPort, rejectingManualFileRenamePort(),
- noOpHistoricalFileNamePort());
+ noOpHistoricalDocumentContextPort());
}
/**
@@ -192,7 +192,7 @@ public record GuiStartupContext(
modelCatalogPort, apiKeyResolutionPort, providerTechnicalTestService, pathCheckPort,
technicalTestOrchestrator, correctionExecutionService, batchRunLauncher,
rejectingMiniRunLauncher(), rejectingResetPort(), rejectingManualFileRenamePort(),
- noOpHistoricalFileNamePort());
+ noOpHistoricalDocumentContextPort());
}
/**
@@ -227,7 +227,7 @@ public record GuiStartupContext(
modelCatalogPort, apiKeyResolutionPort, providerTechnicalTestService, pathCheckPort,
technicalTestOrchestrator, correctionExecutionService,
rejectingBatchRunLauncher(), rejectingMiniRunLauncher(), rejectingResetPort(),
- rejectingManualFileRenamePort(), noOpHistoricalFileNamePort());
+ rejectingManualFileRenamePort(), noOpHistoricalDocumentContextPort());
}
private static GuiBatchRunLauncher rejectingBatchRunLauncher() {
@@ -256,7 +256,7 @@ public record GuiStartupContext(
"Kein Umbennennungs-Port in diesem Startkontext verfügbar.");
}
- private static GuiHistoricalFileNamePort noOpHistoricalFileNamePort() {
+ private static GuiHistoricalDocumentContextPort noOpHistoricalDocumentContextPort() {
return (configPath, fingerprint) -> java.util.Optional.empty();
}
@@ -334,6 +334,6 @@ public record GuiStartupContext(
rejectingMiniRunLauncher(),
rejectingResetPort(),
rejectingManualFileRenamePort(),
- noOpHistoricalFileNamePort());
+ noOpHistoricalDocumentContextPort());
}
}
diff --git a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/FileNameEditorPane.java b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/FileNameEditorPane.java
index add4aa4..025959d 100644
--- a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/FileNameEditorPane.java
+++ b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/FileNameEditorPane.java
@@ -144,7 +144,7 @@ public final class FileNameEditorPane {
*
* Der KI-Vorschlag wird aus {@link GuiBatchRunResultRow#finalFileName()} abgeleitet,
* der letzte gespeicherte Name aus {@link GuiBatchRunResultRow#effectiveFileName()}.
- * Bei nicht editierbaren Status (FAILED_*, SKIPPED, reset-pending, kein SUCCESS)
+ * Bei nicht editierbaren Status (FAILED_*, SKIPPED_*, reset-pending, kein SUCCESS)
* wird das Feld deaktiviert.
*
* @param row die neu selektierte Zeile; {@code null} führt zu {@link #clearSelection()}
diff --git a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/GuiBatchRunCoordinator.java b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/GuiBatchRunCoordinator.java
index 8616360..364f485 100644
--- a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/GuiBatchRunCoordinator.java
+++ b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/GuiBatchRunCoordinator.java
@@ -18,6 +18,7 @@ import de.gecheckt.pdf.umbenenner.application.port.in.BatchRunCancellationToken;
import de.gecheckt.pdf.umbenenner.application.port.in.BatchRunProgressObserver;
import de.gecheckt.pdf.umbenenner.application.port.in.DocumentCompletionEvent;
import de.gecheckt.pdf.umbenenner.application.port.in.DocumentCompletionStatus;
+import de.gecheckt.pdf.umbenenner.application.port.in.HistoricalDocumentContext;
import de.gecheckt.pdf.umbenenner.application.port.in.ResetDocumentStatusResult;
import de.gecheckt.pdf.umbenenner.application.port.in.RunSummary;
import de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint;
@@ -113,7 +114,7 @@ public final class GuiBatchRunCoordinator {
private final Function threadFactory;
private final Consumer fxDispatcher;
private final Listener listener;
- private final GuiHistoricalFileNamePort historicalFileNamePort;
+ private final GuiHistoricalDocumentContextPort historicalDocumentContextPort;
private final AtomicReference activeWorker = new AtomicReference<>();
private final AtomicBoolean cancellationRequested = new AtomicBoolean();
@@ -163,16 +164,16 @@ public final class GuiBatchRunCoordinator {
* @param resetPort bridge to Bootstrap for status-reset-only operations; must
* not be null
* @param listener GUI listener invoked on the FX thread; must not be null
- * @param historicalFileNamePort port for resolving the historical AI-proposed filename for
+ * @param historicalDocumentContextPort port for resolving the historical AI-proposed filename for
* skipped documents; must not be null
*/
public GuiBatchRunCoordinator(GuiBatchRunLauncher launcher,
GuiMiniRunLauncher miniRunLauncher,
GuiResetDocumentStatusPort resetPort,
Listener listener,
- GuiHistoricalFileNamePort historicalFileNamePort) {
+ GuiHistoricalDocumentContextPort historicalDocumentContextPort) {
this(launcher, miniRunLauncher, resetPort,
- defaultThreadFactory(), defaultFxDispatcher(), listener, historicalFileNamePort);
+ defaultThreadFactory(), defaultFxDispatcher(), listener, historicalDocumentContextPort);
}
/**
@@ -200,7 +201,7 @@ public final class GuiBatchRunCoordinator {
Consumer fxDispatcher,
Listener listener) {
this(launcher, miniRunLauncher, resetPort, threadFactory, fxDispatcher, listener,
- noOpHistoricalFileNamePort());
+ noOpHistoricalDocumentContextPort());
}
/**
@@ -218,7 +219,7 @@ public final class GuiBatchRunCoordinator {
* @param fxDispatcher dispatcher that schedules a runnable on the JavaFX Application
* Thread; must not be null
* @param listener GUI listener; must not be null
- * @param historicalFileNamePort port for resolving the historical AI-proposed filename for
+ * @param historicalDocumentContextPort port for resolving the historical AI-proposed filename for
* skipped documents; must not be null
*/
public GuiBatchRunCoordinator(GuiBatchRunLauncher launcher,
@@ -227,15 +228,15 @@ public final class GuiBatchRunCoordinator {
Function threadFactory,
Consumer fxDispatcher,
Listener listener,
- GuiHistoricalFileNamePort historicalFileNamePort) {
+ GuiHistoricalDocumentContextPort historicalDocumentContextPort) {
this.launcher = Objects.requireNonNull(launcher, "launcher must not be null");
this.miniRunLauncher = Objects.requireNonNull(miniRunLauncher, "miniRunLauncher must not be null");
this.resetPort = Objects.requireNonNull(resetPort, "resetPort must not be null");
this.threadFactory = Objects.requireNonNull(threadFactory, "threadFactory must not be null");
this.fxDispatcher = Objects.requireNonNull(fxDispatcher, "fxDispatcher must not be null");
this.listener = Objects.requireNonNull(listener, "listener must not be null");
- this.historicalFileNamePort = Objects.requireNonNull(
- historicalFileNamePort, "historicalFileNamePort must not be null");
+ this.historicalDocumentContextPort = Objects.requireNonNull(
+ historicalDocumentContextPort, "historicalDocumentContextPort must not be null");
}
/**
@@ -555,10 +556,12 @@ public final class GuiBatchRunCoordinator {
/**
* Wandelt ein {@link DocumentCompletionEvent} in eine {@link GuiBatchRunResultRow} um.
*
- * Für übersprungene Dokumente ({@link DocumentCompletionStatus#SKIPPED}) ohne
- * vorhandenen Dateinamen wird der historische KI-Dateiname über den
- * {@link GuiHistoricalFileNamePort} nachgeladen. Schlägt die Abfrage fehl, bleibt
- * die Spalte leer. Die Methode läuft auf dem Worker-Thread.
+ * Für übersprungene Dokumente ({@link DocumentCompletionStatus#SKIPPED_ALREADY_PROCESSED}
+ * und {@link DocumentCompletionStatus#SKIPPED_FINAL_FAILURE}) wird der historische
+ * Verarbeitungskontext über den {@link GuiHistoricalDocumentContextPort} nachgeladen.
+ * Für SKIPPED_ALREADY_PROCESSED wird der letzte Zieldateiname aus dem Kontext als
+ * {@code finalName} übernommen. Schlägt die Abfrage fehl, bleibt der Kontext leer.
+ * Die Methode läuft auf dem Worker-Thread.
*
* @param event das abgeschlossene Kandidatenereignis; darf nicht {@code null} sein
* @param configFilePath Pfad zur aktiven Konfigurationsdatei; darf nicht {@code null} sein
@@ -567,16 +570,6 @@ public final class GuiBatchRunCoordinator {
private GuiBatchRunResultRow toRow(DocumentCompletionEvent event, Path configFilePath) {
Optional finalName = event.finalFileName() == null
? Optional.empty() : Optional.of(event.finalFileName());
- // Historischen KI-Dateinamen für übersprungene Dokumente nachladen
- if (finalName.isEmpty() && event.status() == DocumentCompletionStatus.SKIPPED) {
- try {
- finalName = historicalFileNamePort.resolveHistoricalFileName(
- configFilePath, event.fingerprint());
- } catch (Exception e) {
- LOG.warn("Historischer Dateiname konnte nicht abgefragt werden für {}: {}",
- event.originalFileName(), e.getMessage(), e);
- }
- }
Optional date = event.resolvedDate() == null
? Optional.empty() : Optional.of(event.resolvedDate());
Optional reasoning = event.aiReasoning() == null || event.aiReasoning().isBlank()
@@ -584,18 +577,41 @@ public final class GuiBatchRunCoordinator {
Optional failureMessage = event.failureMessage() == null || event.failureMessage().isBlank()
? Optional.empty() : Optional.of(event.failureMessage());
Duration duration = event.processingDuration();
+
+ // Historischen Kontext für übersprungene Dokumente nachladen
+ boolean isSkipped = event.status() == DocumentCompletionStatus.SKIPPED_ALREADY_PROCESSED
+ || event.status() == DocumentCompletionStatus.SKIPPED_FINAL_FAILURE;
+ Optional historicalContext = Optional.empty();
+ if (isSkipped) {
+ try {
+ historicalContext = historicalDocumentContextPort
+ .resolveHistoricalDocumentContext(configFilePath, event.fingerprint());
+ } catch (Exception e) {
+ LOG.warn("Historischer Kontext konnte nicht abgefragt werden für {}: {}",
+ event.originalFileName(), e.getMessage(), e);
+ }
+ // Zieldateiname für SKIPPED_ALREADY_PROCESSED aus Kontext übernehmen
+ if (finalName.isEmpty()) {
+ finalName = historicalContext
+ .flatMap(HistoricalDocumentContext::lastTargetFileName);
+ }
+ }
+
return new GuiBatchRunResultRow(
event.originalFileName(),
event.fingerprint(),
event.status(),
finalName,
+ Optional.empty(),
date,
reasoning,
failureMessage,
- duration);
+ duration,
+ false,
+ historicalContext);
}
- private static GuiHistoricalFileNamePort noOpHistoricalFileNamePort() {
+ private static GuiHistoricalDocumentContextPort noOpHistoricalDocumentContextPort() {
return (configPath, fingerprint) -> Optional.empty();
}
diff --git a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/GuiBatchRunResultRow.java b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/GuiBatchRunResultRow.java
index e568926..af750cd 100644
--- a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/GuiBatchRunResultRow.java
+++ b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/GuiBatchRunResultRow.java
@@ -6,6 +6,7 @@ import java.util.Objects;
import java.util.Optional;
import de.gecheckt.pdf.umbenenner.application.port.in.DocumentCompletionStatus;
+import de.gecheckt.pdf.umbenenner.application.port.in.HistoricalDocumentContext;
import de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint;
/**
@@ -44,6 +45,8 @@ import de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint;
* never {@code null} and never negative
* @param resetPending {@code true} when the document's persistence status has been
* reset and is awaiting the next processing run
+ * @param historicalContext historischer Verarbeitungskontext für übersprungene Dokumente;
+ * leer bei nicht-übersprungenen Zeilen
*/
public record GuiBatchRunResultRow(
String originalFileName,
@@ -55,7 +58,8 @@ public record GuiBatchRunResultRow(
Optional aiReasoning,
Optional aiFailureMessage,
Duration processingDuration,
- boolean resetPending) {
+ boolean resetPending,
+ Optional historicalContext) {
/**
* Label shown in the status column when a document's persistence status has been
@@ -93,11 +97,12 @@ public record GuiBatchRunResultRow(
if (processingDuration.isNegative()) {
throw new IllegalArgumentException("processingDuration must not be negative");
}
+ historicalContext = historicalContext == null ? Optional.empty() : historicalContext;
}
/**
* Bequem-Konstruktor für Zeilen, die weder einen manuell korrigierten Dateinamen
- * tragen noch im reset-pending-Zustand stehen.
+ * tragen noch im reset-pending-Zustand stehen und keinen historischen Kontext haben.
*
* @param originalFileName the source filename; never {@code null} or blank
* @param fingerprint the content-based document identity; never {@code null}
@@ -122,12 +127,13 @@ public record GuiBatchRunResultRow(
Optional aiFailureMessage,
Duration processingDuration) {
this(originalFileName, fingerprint, status, finalFileName, Optional.empty(),
- resolvedDate, aiReasoning, aiFailureMessage, processingDuration, false);
+ resolvedDate, aiReasoning, aiFailureMessage, processingDuration, false,
+ Optional.empty());
}
/**
* Bequem-Konstruktor mit explizitem {@code resetPending}-Flag, aber ohne manuell
- * korrigierten Dateinamen.
+ * korrigierten Dateinamen und ohne historischen Kontext.
*
* @param originalFileName the source filename; never {@code null} or blank
* @param fingerprint the content-based document identity; never {@code null}
@@ -154,7 +160,8 @@ public record GuiBatchRunResultRow(
Duration processingDuration,
boolean resetPending) {
this(originalFileName, fingerprint, status, finalFileName, Optional.empty(),
- resolvedDate, aiReasoning, aiFailureMessage, processingDuration, resetPending);
+ resolvedDate, aiReasoning, aiFailureMessage, processingDuration, resetPending,
+ Optional.empty());
}
/**
@@ -178,8 +185,10 @@ public record GuiBatchRunResultRow(
Optional.empty(),
Optional.empty(),
Optional.empty(),
+ Optional.empty(),
Duration.ZERO,
- true);
+ true,
+ Optional.empty());
}
/**
@@ -199,7 +208,8 @@ public record GuiBatchRunResultRow(
case SUCCESS -> "\u2714"; // ✔ HEAVY CHECK MARK
case FAILED_RETRYABLE -> "\u26A0"; // ⚠ WARNING SIGN
case FAILED_PERMANENT -> "\u2718"; // ✘ HEAVY BALLOT X
- case SKIPPED -> "\u25BA"; // ► BLACK RIGHT-POINTING POINTER
+ case SKIPPED_ALREADY_PROCESSED,
+ SKIPPED_FINAL_FAILURE -> "\u25BA"; // ► BLACK RIGHT-POINTING POINTER
};
}
@@ -219,7 +229,8 @@ public record GuiBatchRunResultRow(
case SUCCESS -> "Erfolgreich";
case FAILED_RETRYABLE -> "Fehlgeschlagen (wiederholbar)";
case FAILED_PERMANENT -> "Fehlgeschlagen (permanent)";
- case SKIPPED -> "Übersprungen";
+ case SKIPPED_ALREADY_PROCESSED -> "Übersprungen (bereits verarbeitet)";
+ case SKIPPED_FINAL_FAILURE -> "Übersprungen (endgültig fehlgeschlagen)";
};
}
diff --git a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/GuiBatchRunTab.java b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/GuiBatchRunTab.java
index a3f8aa6..2420037 100644
--- a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/GuiBatchRunTab.java
+++ b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/GuiBatchRunTab.java
@@ -2,7 +2,9 @@ package de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun;
import java.nio.file.Path;
import java.time.Duration;
+import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
+import java.util.Locale;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
@@ -193,7 +195,7 @@ public final class GuiBatchRunTab {
private final Runnable onRunStateChanged;
private final GuiBatchRunCoordinator coordinator;
private final Supplier manualFileRenamePortSupplier;
- private final Supplier historicalFileNamePortSupplier;
+ private final Supplier historicalDocumentContextPortSupplier;
private final Supplier> sourceFolderSupplier;
private final Supplier> targetFolderSupplier;
@@ -232,8 +234,8 @@ public final class GuiBatchRunTab {
* null sein
* @param manualFileRenamePortSupplier Supplier für den manuellen Umbennennungs-Port;
* darf nicht null sein
- * @param historicalFileNamePortSupplier Supplier für den historischen Dateiname-Port;
- * darf nicht null sein
+ * @param historicalDocumentContextPortSupplier Supplier für den historischen Kontext-Port;
+ * darf nicht null sein
* @param sourceFolderSupplier Supplier für den konfigurierten Quellordner;
* darf leeres Optional zurückliefern
* @param targetFolderSupplier Supplier für den konfigurierten Zielordner als
@@ -246,7 +248,7 @@ public final class GuiBatchRunTab {
BooleanSupplier savedConfigurationReadyCheck,
Runnable onRunStateChanged,
Supplier manualFileRenamePortSupplier,
- Supplier historicalFileNamePortSupplier,
+ Supplier historicalDocumentContextPortSupplier,
Supplier> sourceFolderSupplier,
Supplier> targetFolderSupplier) {
Objects.requireNonNull(launcherSupplier, "launcherSupplier must not be null");
@@ -258,8 +260,8 @@ public final class GuiBatchRunTab {
this.onRunStateChanged = Objects.requireNonNull(onRunStateChanged, "onRunStateChanged must not be null");
this.manualFileRenamePortSupplier = Objects.requireNonNull(
manualFileRenamePortSupplier, "manualFileRenamePortSupplier must not be null");
- this.historicalFileNamePortSupplier = Objects.requireNonNull(
- historicalFileNamePortSupplier, "historicalFileNamePortSupplier must not be null");
+ this.historicalDocumentContextPortSupplier = Objects.requireNonNull(
+ historicalDocumentContextPortSupplier, "historicalDocumentContextPortSupplier must not be null");
this.sourceFolderSupplier = Objects.requireNonNull(
sourceFolderSupplier, "sourceFolderSupplier must not be null");
this.targetFolderSupplier = Objects.requireNonNull(
@@ -273,7 +275,7 @@ public final class GuiBatchRunTab {
(configPath, fingerprints) ->
resetPortSupplier.get().reset(configPath, fingerprints),
new CoordinatorListener(),
- historicalFileNamePortSupplier.get());
+ historicalDocumentContextPortSupplier.get());
this.tab.setClosable(false);
this.tab.setContent(buildContent());
@@ -799,7 +801,8 @@ public final class GuiBatchRunTab {
row.aiReasoning(),
row.aiFailureMessage(),
row.processingDuration(),
- row.resetPending());
+ row.resetPending(),
+ row.historicalContext());
currentlySelectedRow = updatedRow;
// Dirty-State vor dem Zeilen-Upsert zurücksetzen, damit das folgende
// resultItems.set() keinen Verwerfen-Dialog über den selectedItemProperty-Listener
@@ -1216,7 +1219,7 @@ public final class GuiBatchRunTab {
case SUCCESS -> "#2e7d32";
case FAILED_RETRYABLE -> "#e65100";
case FAILED_PERMANENT -> "#c62828";
- case SKIPPED -> "#757575";
+ case SKIPPED_ALREADY_PROCESSED, SKIPPED_FINAL_FAILURE -> "#757575";
};
}
@@ -1228,6 +1231,9 @@ public final class GuiBatchRunTab {
return String.format("%.1f s", seconds);
}
+ private static final DateTimeFormatter DETAIL_DATE_FORMAT =
+ DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm", Locale.GERMANY);
+
private static String buildDetailText(GuiBatchRunResultRow row) {
StringBuilder builder = new StringBuilder();
builder.append("Originaldateiname: ").append(row.originalFileName()).append('\n');
@@ -1235,6 +1241,34 @@ public final class GuiBatchRunTab {
builder.append('\n').append(GuiBatchRunResultRow.RESET_PENDING_LABEL);
return builder.toString();
}
+ if (row.status() == DocumentCompletionStatus.SKIPPED_ALREADY_PROCESSED) {
+ builder.append('\n');
+ row.historicalContext().ifPresentOrElse(ctx -> {
+ ctx.lastSuccessInstant().ifPresentOrElse(
+ instant -> builder.append("Bereits erfolgreich verarbeitet am ")
+ .append(DETAIL_DATE_FORMAT.format(
+ instant.atZone(ZoneId.systemDefault())))
+ .append('.'),
+ () -> builder.append("Bereits erfolgreich verarbeitet."));
+ ctx.lastTargetFileName().ifPresent(name ->
+ builder.append('\n').append("Zieldatei: ").append(name).append('.'));
+ }, () -> builder.append("Bereits erfolgreich verarbeitet."));
+ return builder.toString();
+ }
+ if (row.status() == DocumentCompletionStatus.SKIPPED_FINAL_FAILURE) {
+ builder.append('\n');
+ row.historicalContext().ifPresentOrElse(ctx ->
+ ctx.lastFailureInstant().ifPresentOrElse(
+ instant -> builder.append("Endg\u00fcltig fehlgeschlagen am ")
+ .append(DETAIL_DATE_FORMAT.format(
+ instant.atZone(ZoneId.systemDefault())))
+ .append(". Erneute Verarbeitung nur nach Reset m\u00f6glich."),
+ () -> builder.append(
+ "Endg\u00fcltig fehlgeschlagen. Erneute Verarbeitung nur nach Reset m\u00f6glich.")),
+ () -> builder.append(
+ "Endg\u00fcltig fehlgeschlagen. Erneute Verarbeitung nur nach Reset m\u00f6glich."));
+ return builder.toString();
+ }
row.effectiveFileName()
.ifPresent(name -> builder.append("Neuer Dateiname: ").append(name).append('\n'));
row.resolvedDate()
@@ -1311,7 +1345,7 @@ public final class GuiBatchRunTab {
switch (row.status()) {
case SUCCESS -> successCount++;
case FAILED_RETRYABLE, FAILED_PERMANENT -> failedCount++;
- case SKIPPED -> skippedCount++;
+ case SKIPPED_ALREADY_PROCESSED, SKIPPED_FINAL_FAILURE -> skippedCount++;
default -> throw new IllegalStateException(
"Unerwarteter Status: " + row.status());
}
diff --git a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/GuiHistoricalDocumentContextPort.java b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/GuiHistoricalDocumentContextPort.java
new file mode 100644
index 0000000..4e27375
--- /dev/null
+++ b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/GuiHistoricalDocumentContextPort.java
@@ -0,0 +1,42 @@
+package de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun;
+
+import java.nio.file.Path;
+import java.util.Optional;
+
+import de.gecheckt.pdf.umbenenner.application.port.in.HistoricalDocumentContext;
+import de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint;
+
+/**
+ * GUI-interner Port zum Abfragen des historischen Verarbeitungskontexts einer Quelldatei.
+ *
+ * Wird im Verarbeitungslauf-Tab genutzt, um für übersprungene Dokumente
+ * ({@link de.gecheckt.pdf.umbenenner.application.port.in.DocumentCompletionStatus#SKIPPED_ALREADY_PROCESSED}
+ * und
+ * {@link de.gecheckt.pdf.umbenenner.application.port.in.DocumentCompletionStatus#SKIPPED_FINAL_FAILURE})
+ * den historischen Kontext nachzuschlagen. Der Kontext wird im Detailbereich des
+ * Verarbeitungslauf-Tabs angezeigt.
+ *
+ * Die Bootstrap-Schicht liefert die konkrete Implementierung. Sie lädt die
+ * Konfiguration aus {@code configFilePath}, baut den zugehörigen Use-Case auf und
+ * gibt das Ergebnis zurück. Technische Fehler beim Laden oder Abfragen werden intern
+ * abgefangen und als leeres {@link Optional} zurückgegeben.
+ *
+ * Die Implementierung läuft auf dem Worker-Thread des {@link GuiBatchRunCoordinator}
+ * und darf blockieren.
+ */
+@FunctionalInterface
+public interface GuiHistoricalDocumentContextPort {
+
+ /**
+ * Gibt den historischen Verarbeitungskontext für das durch {@code fingerprint}
+ * identifizierte Dokument zurück, oder ein leeres {@link Optional}, wenn kein
+ * Kontext verfügbar ist.
+ *
+ * @param configFilePath Pfad zur aktiven {@code .properties}-Konfigurationsdatei;
+ * darf nicht {@code null} sein
+ * @param fingerprint inhaltsbasierte Dokumentenidentität; darf nicht {@code null} sein
+ * @return historischer Kontext des Dokuments, oder leer wenn nicht verfügbar
+ */
+ Optional resolveHistoricalDocumentContext(
+ Path configFilePath, DocumentFingerprint fingerprint);
+}
diff --git a/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/FileNameEditorPaneTest.java b/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/FileNameEditorPaneTest.java
index 4924cf7..0e6997b 100644
--- a/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/FileNameEditorPaneTest.java
+++ b/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/FileNameEditorPaneTest.java
@@ -321,7 +321,7 @@ class FileNameEditorPaneTest {
Optional.of("2026-01-01 - KI-Vorschlag.pdf"),
Optional.of("2026-01-01 - Manuell.pdf"),
Optional.empty(), Optional.empty(), Optional.empty(),
- Duration.ofMillis(1), false);
+ Duration.ofMillis(1), false, Optional.empty());
pane.loadSelection(row, "C:\\target");
// lastSavedName = "2026-01-01 - Manuell" (effectiveFileName)
assertEquals("2026-01-01 - Manuell", pane.textField().getText());
diff --git a/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/GuiBatchRunCoordinatorTest.java b/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/GuiBatchRunCoordinatorTest.java
index bccedc6..6e2b91b 100644
--- a/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/GuiBatchRunCoordinatorTest.java
+++ b/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/GuiBatchRunCoordinatorTest.java
@@ -118,7 +118,7 @@ class GuiBatchRunCoordinatorTest {
GuiBatchRunLauncher launcher = (configPath, observer, token) -> {
observer.onRunStarted(new RunId("run-skip"), 1);
observer.onDocumentCompleted(new DocumentCompletionEvent(
- "c.pdf", DUMMY_FP, DocumentCompletionStatus.SKIPPED,
+ "c.pdf", DUMMY_FP, DocumentCompletionStatus.SKIPPED_ALREADY_PROCESSED,
null, null, null, null, Duration.ofMillis(5)));
observer.onRunEnded(new RunSummary(0, 0, 1));
return GuiBatchRunLaunchOutcome.completed();
@@ -131,7 +131,7 @@ class GuiBatchRunCoordinatorTest {
assertEquals(List.of(
"started:1",
- "row:SKIPPED:c.pdf",
+ "row:SKIPPED_ALREADY_PROCESSED:c.pdf",
"ended:started=true,completed=true,summary=0/0/1"), events);
assertFalse(coordinator.isRunning());
}
@@ -290,7 +290,7 @@ class GuiBatchRunCoordinatorTest {
assertEquals("\u2714", row(DocumentCompletionStatus.SUCCESS).statusIcon());
assertEquals("\u26A0", row(DocumentCompletionStatus.FAILED_RETRYABLE).statusIcon());
assertEquals("\u2718", row(DocumentCompletionStatus.FAILED_PERMANENT).statusIcon());
- assertEquals("\u25BA", row(DocumentCompletionStatus.SKIPPED).statusIcon());
+ assertEquals("\u25BA", row(DocumentCompletionStatus.SKIPPED_ALREADY_PROCESSED).statusIcon());
}
@Test
diff --git a/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/GuiBatchRunResultRowTest.java b/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/GuiBatchRunResultRowTest.java
index 5290b50..270569c 100644
--- a/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/GuiBatchRunResultRowTest.java
+++ b/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/GuiBatchRunResultRowTest.java
@@ -109,8 +109,13 @@ class GuiBatchRunResultRowTest {
}
@Test
- void statusIcon_skipped_isPointer() {
- assertEquals("\u25BA", row(DocumentCompletionStatus.SKIPPED).statusIcon());
+ void statusIcon_skippedAlreadyProcessed_isPointer() {
+ assertEquals("\u25BA", row(DocumentCompletionStatus.SKIPPED_ALREADY_PROCESSED).statusIcon());
+ }
+
+ @Test
+ void statusIcon_skippedFinalFailure_isPointer() {
+ assertEquals("\u25BA", row(DocumentCompletionStatus.SKIPPED_FINAL_FAILURE).statusIcon());
}
// -------------------------------------------------------------------------
diff --git a/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/GuiBatchRunTabSelectionSmokeTest.java b/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/GuiBatchRunTabSelectionSmokeTest.java
index 346b63a..42953f6 100644
--- a/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/GuiBatchRunTabSelectionSmokeTest.java
+++ b/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/GuiBatchRunTabSelectionSmokeTest.java
@@ -84,7 +84,7 @@ class GuiBatchRunTabSelectionSmokeTest {
runOnFx(() -> {
GuiBatchRunTab tab = makeTab();
tab.upsertResultRowByFingerprint(row("a.pdf", FP1, DocumentCompletionStatus.SUCCESS));
- tab.upsertResultRowByFingerprint(row("b.pdf", FP2, DocumentCompletionStatus.SKIPPED));
+ tab.upsertResultRowByFingerprint(row("b.pdf", FP2, DocumentCompletionStatus.SKIPPED_ALREADY_PROCESSED));
tab.upsertResultRowByFingerprint(row("c.pdf", FP3, DocumentCompletionStatus.FAILED_PERMANENT));
assertEquals(3, tab.resultTable().getItems().size());
});
diff --git a/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/GuiBatchRunTabSmokeTest.java b/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/GuiBatchRunTabSmokeTest.java
index 06d7eb5..adc3513 100644
--- a/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/GuiBatchRunTabSmokeTest.java
+++ b/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/GuiBatchRunTabSmokeTest.java
@@ -107,7 +107,7 @@ class GuiBatchRunTabSmokeTest {
"b.pdf", DUMMY_FP, DocumentCompletionStatus.FAILED_RETRYABLE,
null, null, null, null, Duration.ofMillis(10)));
observer.onDocumentCompleted(new DocumentCompletionEvent(
- "c.pdf", DUMMY_FP, DocumentCompletionStatus.SKIPPED,
+ "c.pdf", DUMMY_FP, DocumentCompletionStatus.SKIPPED_ALREADY_PROCESSED,
null, null, null, null, Duration.ofMillis(5)));
observer.onRunEnded(new RunSummary(1, 1, 1));
return GuiBatchRunLaunchOutcome.completed();
@@ -142,7 +142,7 @@ class GuiBatchRunTabSmokeTest {
// SKIPPED row must carry the ► icon, not ✘.
GuiBatchRunResultRow skippedRow = tab().resultTable().getItems().get(2);
- assertEquals(DocumentCompletionStatus.SKIPPED, skippedRow.status());
+ assertEquals(DocumentCompletionStatus.SKIPPED_ALREADY_PROCESSED, skippedRow.status());
assertEquals("\u25BA", skippedRow.statusIcon());
});
}
diff --git a/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/in/DocumentCompletionStatus.java b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/in/DocumentCompletionStatus.java
index 3a31dcb..b25ab4e 100644
--- a/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/in/DocumentCompletionStatus.java
+++ b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/in/DocumentCompletionStatus.java
@@ -5,10 +5,10 @@ package de.gecheckt.pdf.umbenenner.application.port.in;
* {@link BatchRunProgressObserver#onDocumentCompleted(DocumentCompletionEvent)}
* for one processed candidate.
*
- * This enum collapses the finer-grained internal processing status into the four
+ * This enum collapses the finer-grained internal processing status into five
* buckets that an observer (e.g. a GUI progress view) needs to distinguish:
- * successful completion, retryable failure, permanent failure, and an explicit
- * skip.
+ * successful completion, retryable failure, permanent failure, and two explicit
+ * skip variants.
*
* This classification is purely an observability concern — persistence,
* retry decisions, and all other processing rules continue to work against the
@@ -36,8 +36,16 @@ public enum DocumentCompletionStatus {
FAILED_PERMANENT,
/**
- * The candidate was skipped because it was already in a terminal state (either
- * previously successful or previously finally failed).
+ * Der Kandidat wurde übersprungen, weil er in einem früheren Lauf bereits
+ * erfolgreich verarbeitet wurde. Der Gesamtstatus im Stammsatz lautet
+ * {@code SUCCESS}.
*/
- SKIPPED
+ SKIPPED_ALREADY_PROCESSED,
+
+ /**
+ * Der Kandidat wurde übersprungen, weil er in einem früheren Lauf bereits
+ * endgültig fehlgeschlagen ist. Der Gesamtstatus im Stammsatz lautet
+ * {@code FAILED_FINAL}.
+ */
+ SKIPPED_FINAL_FAILURE
}
diff --git a/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/in/HistoricalDocumentContext.java b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/in/HistoricalDocumentContext.java
new file mode 100644
index 0000000..8d2a9ea
--- /dev/null
+++ b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/in/HistoricalDocumentContext.java
@@ -0,0 +1,75 @@
+package de.gecheckt.pdf.umbenenner.application.port.in;
+
+import java.time.Instant;
+import java.util.Objects;
+import java.util.Optional;
+
+/**
+ * Historischer Kontext eines Dokuments, das in einem früheren Lauf bereits terminal
+ * abgeschlossen wurde.
+ *
+ * Wird genutzt, um im GUI-Detailbereich bei übersprungenen Dokumenten Informationen
+ * darüber anzuzeigen, wann und mit welchem Ergebnis die Datei früher verarbeitet wurde.
+ *
+ * Für Dokumente mit früherem Erfolgsstatus ({@code wasEverSuccessful == true}) sind
+ * {@code lastTargetFileName} und {@code lastSuccessInstant} belegt.
+ * Für Dokumente, die endgültig fehlgeschlagen sind, ist {@code lastFailureInstant} belegt.
+ *
+ * @param lastTargetFileName letzter erfolgreich geschriebener Zieldateiname; leer wenn
+ * das Dokument nie erfolgreich kopiert wurde
+ * @param lastSuccessInstant Zeitpunkt der letzten erfolgreichen Verarbeitung; leer wenn
+ * das Dokument nie erfolgreich verarbeitet wurde
+ * @param lastFailureInstant Zeitpunkt des letzten Fehlschlags; leer wenn noch kein
+ * Fehlschlag aufgetreten ist
+ * @param wasEverSuccessful {@code true} wenn das Dokument mindestens einmal erfolgreich
+ * verarbeitet wurde
+ */
+public record HistoricalDocumentContext(
+ Optional lastTargetFileName,
+ Optional lastSuccessInstant,
+ Optional lastFailureInstant,
+ boolean wasEverSuccessful) {
+
+ /**
+ * Kompakter Konstruktor zur Normalisierung von {@code null}-Werten.
+ *
+ * @throws NullPointerException wenn {@code lastTargetFileName},
+ * {@code lastSuccessInstant} oder
+ * {@code lastFailureInstant} {@code null} sind
+ */
+ public HistoricalDocumentContext {
+ lastTargetFileName = lastTargetFileName == null ? Optional.empty() : lastTargetFileName;
+ lastSuccessInstant = lastSuccessInstant == null ? Optional.empty() : lastSuccessInstant;
+ lastFailureInstant = lastFailureInstant == null ? Optional.empty() : lastFailureInstant;
+ }
+
+ /**
+ * Erstellt einen Kontext für ein erfolgreich verarbeitetes Dokument.
+ *
+ * @param lastTargetFileName letzter Zieldateiname; darf {@code null} sein
+ * @param lastSuccessInstant Zeitpunkt des Erfolgs; darf {@code null} sein
+ * @return neuer Kontext mit {@code wasEverSuccessful == true}
+ */
+ public static HistoricalDocumentContext ofSuccess(
+ String lastTargetFileName, Instant lastSuccessInstant) {
+ return new HistoricalDocumentContext(
+ Optional.ofNullable(lastTargetFileName),
+ Optional.ofNullable(lastSuccessInstant),
+ Optional.empty(),
+ true);
+ }
+
+ /**
+ * Erstellt einen Kontext für ein endgültig fehlgeschlagenes Dokument.
+ *
+ * @param lastFailureInstant Zeitpunkt des letzten Fehlschlags; darf {@code null} sein
+ * @return neuer Kontext mit {@code wasEverSuccessful == false}
+ */
+ public static HistoricalDocumentContext ofFinalFailure(Instant lastFailureInstant) {
+ return new HistoricalDocumentContext(
+ Optional.empty(),
+ Optional.empty(),
+ Optional.ofNullable(lastFailureInstant),
+ false);
+ }
+}
diff --git a/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/in/ResolveHistoricalDocumentContextUseCase.java b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/in/ResolveHistoricalDocumentContextUseCase.java
new file mode 100644
index 0000000..ccb8325
--- /dev/null
+++ b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/in/ResolveHistoricalDocumentContextUseCase.java
@@ -0,0 +1,42 @@
+package de.gecheckt.pdf.umbenenner.application.port.in;
+
+import java.util.Optional;
+
+import de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint;
+
+/**
+ * Inbound-Port zum Abfragen des historischen Verarbeitungskontexts eines Dokuments.
+ *
+ * Wird im GUI-Verarbeitungslauf-Tab eingesetzt, um für übersprungene Dokumente
+ * anzuzeigen, wann und mit welchem Ergebnis die Datei in einem früheren Lauf
+ * verarbeitet wurde.
+ *
+ * Für Dokumente mit früherem Erfolgsstatus enthält der zurückgegebene Kontext den
+ * letzten Zieldateinamen und den Erfolgszeitpunkt. Für endgültig fehlgeschlagene
+ * Dokumente ist der letzte Fehlzeitpunkt belegt.
+ *
+ * Architekturgrenzen: Implementierungen dieses Ports dürfen keine
+ * JDBC-, SQLite-, Dateisystem- oder HTTP-Typen nach außen exponieren. Alle
+ * Infrastrukturdetails verbleiben in der Adapter-Schicht.
+ */
+public interface ResolveHistoricalDocumentContextUseCase {
+
+ /**
+ * Gibt den historischen Verarbeitungskontext für das durch den Fingerprint
+ * identifizierte Dokument zurück.
+ *
+ * Liefert einen gefüllten Kontext, wenn das Dokument einen früheren terminalen
+ * Abschluss (Erfolg oder endgültiger Fehlschlag) hat. Gibt ein leeres
+ * {@link Optional} zurück, wenn kein passender Stammsatz vorhanden ist oder
+ * die Abfrage technisch fehlschlägt.
+ *
+ * Wirft keine geprüften Ausnahmen: technische Abfragefehler werden intern
+ * abgefangen und als leeres Ergebnis zurückgegeben.
+ *
+ * @param fingerprint inhaltsbasierte Dokumentenidentität; darf nicht {@code null} sein
+ * @return historischer Kontext des Dokuments, oder leer wenn nicht verfügbar
+ * @throws NullPointerException wenn {@code fingerprint} {@code null} ist
+ */
+ Optional resolveHistoricalDocumentContext(
+ DocumentFingerprint fingerprint);
+}
diff --git a/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/in/RunSummary.java b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/in/RunSummary.java
index 0f06b9c..062c0eb 100644
--- a/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/in/RunSummary.java
+++ b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/in/RunSummary.java
@@ -13,8 +13,9 @@ package de.gecheckt.pdf.umbenenner.application.port.in;
* @param failedCount number of candidates that completed with either
* {@link DocumentCompletionStatus#FAILED_RETRYABLE} or
* {@link DocumentCompletionStatus#FAILED_PERMANENT}; must be ≥ 0
- * @param skippedCount number of candidates that completed with
- * {@link DocumentCompletionStatus#SKIPPED}; must be ≥ 0
+ * @param skippedCount number of candidates that completed with either
+ * {@link DocumentCompletionStatus#SKIPPED_ALREADY_PROCESSED} or
+ * {@link DocumentCompletionStatus#SKIPPED_FINAL_FAILURE}; must be ≥ 0
*/
public record RunSummary(int successCount, int failedCount, int skippedCount) {
diff --git a/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/service/DocumentProcessingCoordinator.java b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/service/DocumentProcessingCoordinator.java
index ec1c768..60d9019 100644
--- a/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/service/DocumentProcessingCoordinator.java
+++ b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/service/DocumentProcessingCoordinator.java
@@ -815,7 +815,11 @@ public class DocumentProcessingCoordinator {
logger.debug("Skip attempt #{} persisted for '{}' with status {}.",
attemptNumber, candidate.uniqueIdentifier(), skipStatus);
- publishCompletion(candidate, fingerprint, DocumentCompletionStatus.SKIPPED,
+ DocumentCompletionStatus completionStatus =
+ skipStatus == ProcessingStatus.SKIPPED_ALREADY_PROCESSED
+ ? DocumentCompletionStatus.SKIPPED_ALREADY_PROCESSED
+ : DocumentCompletionStatus.SKIPPED_FINAL_FAILURE;
+ publishCompletion(candidate, fingerprint, completionStatus,
null, null, null, null, attemptStart, now);
return true;
diff --git a/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/usecase/CountingCompletionObserver.java b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/usecase/CountingCompletionObserver.java
index d89043b..9fedf06 100644
--- a/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/usecase/CountingCompletionObserver.java
+++ b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/usecase/CountingCompletionObserver.java
@@ -38,7 +38,7 @@ final class CountingCompletionObserver implements Consumer successCount++;
case FAILED_RETRYABLE, FAILED_PERMANENT -> failedCount++;
- case SKIPPED -> skippedCount++;
+ case SKIPPED_ALREADY_PROCESSED, SKIPPED_FINAL_FAILURE -> skippedCount++;
default -> {
// Defensive — new status values would be a programming error.
throw new IllegalStateException(
diff --git a/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/usecase/DefaultResolveHistoricalDocumentContextUseCase.java b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/usecase/DefaultResolveHistoricalDocumentContextUseCase.java
new file mode 100644
index 0000000..154ce38
--- /dev/null
+++ b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/usecase/DefaultResolveHistoricalDocumentContextUseCase.java
@@ -0,0 +1,83 @@
+package de.gecheckt.pdf.umbenenner.application.usecase;
+
+import java.util.Objects;
+import java.util.Optional;
+
+import de.gecheckt.pdf.umbenenner.application.port.in.HistoricalDocumentContext;
+import de.gecheckt.pdf.umbenenner.application.port.in.ResolveHistoricalDocumentContextUseCase;
+import de.gecheckt.pdf.umbenenner.application.port.out.DocumentRecordLookupResult;
+import de.gecheckt.pdf.umbenenner.application.port.out.DocumentRecordRepository;
+import de.gecheckt.pdf.umbenenner.application.port.out.DocumentTerminalFinalFailure;
+import de.gecheckt.pdf.umbenenner.application.port.out.DocumentTerminalSuccess;
+import de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint;
+
+/**
+ * Standardimplementierung von {@link ResolveHistoricalDocumentContextUseCase}.
+ *
+ * Fragt den {@link DocumentRecordRepository} nach dem Stammsatz des angegebenen
+ * Fingerprints ab. Abhängig vom terminalen Zustand des Dokuments wird ein passender
+ * {@link HistoricalDocumentContext} zurückgegeben:
+ *
+ * - Bei früherem Erfolgsstatus: Zieldateiname und Erfolgszeitpunkt aus dem
+ * Stammsatz ({@code lastTargetFileName}, {@code lastSuccessInstant}).
+ * - Bei endgültigem Fehlschlag: Fehlzeitpunkt aus dem Stammsatz
+ * ({@code lastFailureInstant}).
+ * - In allen anderen Fällen (unbekannt, verarbeitbar) sowie bei technischen
+ * Abfragefehlern: leeres {@link Optional}.
+ *
+ * Technische Fehler bei der Repository-Abfrage werden intern abgefangen; der Aufrufer
+ * erhält stets ein leeres Ergebnis statt einer Ausnahme.
+ */
+public class DefaultResolveHistoricalDocumentContextUseCase
+ implements ResolveHistoricalDocumentContextUseCase {
+
+ private final DocumentRecordRepository documentRecordRepository;
+
+ /**
+ * Erstellt den Use Case mit dem erforderlichen Dokument-Stammsatz-Repository.
+ *
+ * @param documentRecordRepository Repository zum Lesen von Dokument-Stammsätzen;
+ * darf nicht {@code null} sein
+ * @throws NullPointerException wenn {@code documentRecordRepository} {@code null} ist
+ */
+ public DefaultResolveHistoricalDocumentContextUseCase(
+ DocumentRecordRepository documentRecordRepository) {
+ this.documentRecordRepository = Objects.requireNonNull(
+ documentRecordRepository, "documentRecordRepository must not be null");
+ }
+
+ /**
+ * Gibt den historischen Verarbeitungskontext für das durch den Fingerprint
+ * identifizierte Dokument zurück.
+ *
+ * Für Dokumente mit früherem Erfolgsstatus enthält der Kontext Zieldateiname und
+ * Erfolgszeitpunkt. Für endgültig fehlgeschlagene Dokumente ist der Fehlzeitpunkt
+ * belegt. Für alle anderen Zustände oder bei Abfragefehlern wird ein leeres
+ * {@link Optional} zurückgegeben.
+ *
+ * @param fingerprint inhaltsbasierte Dokumentenidentität; darf nicht {@code null} sein
+ * @return historischer Kontext des Dokuments, oder leer wenn nicht verfügbar
+ * @throws NullPointerException wenn {@code fingerprint} {@code null} ist
+ */
+ @Override
+ public Optional resolveHistoricalDocumentContext(
+ DocumentFingerprint fingerprint) {
+ Objects.requireNonNull(fingerprint, "fingerprint must not be null");
+ try {
+ DocumentRecordLookupResult result =
+ documentRecordRepository.findByFingerprint(fingerprint);
+ if (result instanceof DocumentTerminalSuccess success) {
+ return Optional.of(HistoricalDocumentContext.ofSuccess(
+ success.record().lastTargetFileName(),
+ success.record().lastSuccessInstant()));
+ }
+ if (result instanceof DocumentTerminalFinalFailure failure) {
+ return Optional.of(HistoricalDocumentContext.ofFinalFailure(
+ failure.record().lastFailureInstant()));
+ }
+ return Optional.empty();
+ } catch (Exception e) {
+ return Optional.empty();
+ }
+ }
+}
diff --git a/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/usecase/BatchRunProgressObservationTest.java b/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/usecase/BatchRunProgressObservationTest.java
index f40717d..59bce43 100644
--- a/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/usecase/BatchRunProgressObservationTest.java
+++ b/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/usecase/BatchRunProgressObservationTest.java
@@ -129,7 +129,7 @@ class BatchRunProgressObservationTest {
assertSame(a, b);
a.onRunStarted(new RunId("r-1"), 5);
a.onDocumentCompleted(new DocumentCompletionEvent(
- "x.pdf", DUMMY_FP, DocumentCompletionStatus.SKIPPED, null, null, null, null, Duration.ZERO));
+ "x.pdf", DUMMY_FP, DocumentCompletionStatus.SKIPPED_ALREADY_PROCESSED, null, null, null, null, Duration.ZERO));
a.onRunEnded(new RunSummary(0, 0, 0));
}
@@ -166,7 +166,7 @@ class BatchRunProgressObservationTest {
DocumentCompletionStatus.SUCCESS,
DocumentCompletionStatus.FAILED_RETRYABLE,
DocumentCompletionStatus.FAILED_PERMANENT,
- DocumentCompletionStatus.SKIPPED));
+ DocumentCompletionStatus.SKIPPED_ALREADY_PROCESSED));
DefaultBatchRunProcessingUseCase useCase = buildUseCase(
new NoOpLock(), new FixedCandidatesPort(
makeCandidate("a.pdf"),
diff --git a/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/usecase/DefaultResolveHistoricalDocumentContextUseCaseTest.java b/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/usecase/DefaultResolveHistoricalDocumentContextUseCaseTest.java
new file mode 100644
index 0000000..09d531b
--- /dev/null
+++ b/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/usecase/DefaultResolveHistoricalDocumentContextUseCaseTest.java
@@ -0,0 +1,180 @@
+package de.gecheckt.pdf.umbenenner.application.usecase;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatNullPointerException;
+
+import java.time.Instant;
+import java.util.Optional;
+
+import org.junit.jupiter.api.Test;
+
+import de.gecheckt.pdf.umbenenner.application.port.in.HistoricalDocumentContext;
+import de.gecheckt.pdf.umbenenner.application.port.out.DocumentKnownProcessable;
+import de.gecheckt.pdf.umbenenner.application.port.out.DocumentPersistenceException;
+import de.gecheckt.pdf.umbenenner.application.port.out.DocumentRecord;
+import de.gecheckt.pdf.umbenenner.application.port.out.DocumentRecordLookupResult;
+import de.gecheckt.pdf.umbenenner.application.port.out.DocumentRecordRepository;
+import de.gecheckt.pdf.umbenenner.application.port.out.DocumentTerminalFinalFailure;
+import de.gecheckt.pdf.umbenenner.application.port.out.DocumentTerminalSuccess;
+import de.gecheckt.pdf.umbenenner.application.port.out.DocumentUnknown;
+import de.gecheckt.pdf.umbenenner.application.port.out.FailureCounters;
+import de.gecheckt.pdf.umbenenner.application.port.out.PersistenceLookupTechnicalFailure;
+import de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint;
+import de.gecheckt.pdf.umbenenner.domain.model.ProcessingStatus;
+import de.gecheckt.pdf.umbenenner.domain.model.SourceDocumentLocator;
+
+/**
+ * Unit tests for {@link DefaultResolveHistoricalDocumentContextUseCase}.
+ */
+class DefaultResolveHistoricalDocumentContextUseCaseTest {
+
+ private static final DocumentFingerprint FP = new DocumentFingerprint("a".repeat(64));
+ private static final Instant NOW = Instant.parse("2026-01-15T10:30:00Z");
+
+ @Test
+ void constructor_withNullRepository_throws() {
+ assertThatNullPointerException()
+ .isThrownBy(() -> new DefaultResolveHistoricalDocumentContextUseCase(null))
+ .withMessageContaining("documentRecordRepository");
+ }
+
+ @Test
+ void resolve_withNullFingerprint_throws() {
+ var useCase = new DefaultResolveHistoricalDocumentContextUseCase(
+ stubRepo(new DocumentUnknown()));
+ assertThatNullPointerException()
+ .isThrownBy(() -> useCase.resolveHistoricalDocumentContext(null))
+ .withMessageContaining("fingerprint");
+ }
+
+ @Test
+ void resolve_forSuccessRecord_returnsContextWithTargetFileNameAndSuccessInstant() {
+ DocumentRecord record = buildRecord(ProcessingStatus.SUCCESS,
+ "2026-01-01 - Rechnung.pdf", NOW, null);
+ var useCase = new DefaultResolveHistoricalDocumentContextUseCase(
+ stubRepo(new DocumentTerminalSuccess(record)));
+
+ Optional result =
+ useCase.resolveHistoricalDocumentContext(FP);
+
+ assertThat(result).isPresent();
+ assertThat(result.get().wasEverSuccessful()).isTrue();
+ assertThat(result.get().lastTargetFileName()).contains("2026-01-01 - Rechnung.pdf");
+ assertThat(result.get().lastSuccessInstant()).contains(NOW);
+ assertThat(result.get().lastFailureInstant()).isEmpty();
+ }
+
+ @Test
+ void resolve_forSuccessRecordWithNullTargetFileName_returnsContextWithEmptyFileName() {
+ DocumentRecord record = buildRecord(ProcessingStatus.SUCCESS, null, NOW, null);
+ var useCase = new DefaultResolveHistoricalDocumentContextUseCase(
+ stubRepo(new DocumentTerminalSuccess(record)));
+
+ Optional result =
+ useCase.resolveHistoricalDocumentContext(FP);
+
+ assertThat(result).isPresent();
+ assertThat(result.get().wasEverSuccessful()).isTrue();
+ assertThat(result.get().lastTargetFileName()).isEmpty();
+ assertThat(result.get().lastSuccessInstant()).contains(NOW);
+ }
+
+ @Test
+ void resolve_forFinalFailureRecord_returnsContextWithFailureInstant() {
+ DocumentRecord record = buildRecord(ProcessingStatus.FAILED_FINAL, null, null, NOW);
+ var useCase = new DefaultResolveHistoricalDocumentContextUseCase(
+ stubRepo(new DocumentTerminalFinalFailure(record)));
+
+ Optional result =
+ useCase.resolveHistoricalDocumentContext(FP);
+
+ assertThat(result).isPresent();
+ assertThat(result.get().wasEverSuccessful()).isFalse();
+ assertThat(result.get().lastFailureInstant()).contains(NOW);
+ assertThat(result.get().lastTargetFileName()).isEmpty();
+ assertThat(result.get().lastSuccessInstant()).isEmpty();
+ }
+
+ @Test
+ void resolve_forUnknownDocument_returnsEmpty() {
+ var useCase = new DefaultResolveHistoricalDocumentContextUseCase(
+ stubRepo(new DocumentUnknown()));
+
+ assertThat(useCase.resolveHistoricalDocumentContext(FP)).isEmpty();
+ }
+
+ @Test
+ void resolve_forProcessableDocument_returnsEmpty() {
+ DocumentRecord record = buildRecord(ProcessingStatus.READY_FOR_AI, null, null, null);
+ var useCase = new DefaultResolveHistoricalDocumentContextUseCase(
+ stubRepo(new DocumentKnownProcessable(record)));
+
+ assertThat(useCase.resolveHistoricalDocumentContext(FP)).isEmpty();
+ }
+
+ @Test
+ void resolve_forPersistenceLookupFailure_returnsEmpty() {
+ var useCase = new DefaultResolveHistoricalDocumentContextUseCase(
+ stubRepo(new PersistenceLookupTechnicalFailure("DB-Fehler", null)));
+
+ assertThat(useCase.resolveHistoricalDocumentContext(FP)).isEmpty();
+ }
+
+ @Test
+ void resolve_whenRepositoryThrows_returnsEmpty() {
+ DocumentRecordRepository throwingRepo = new DocumentRecordRepository() {
+ @Override
+ public DocumentRecordLookupResult findByFingerprint(DocumentFingerprint fingerprint) {
+ throw new DocumentPersistenceException("Verbindungsfehler", null);
+ }
+ @Override
+ public void create(DocumentRecord record) {}
+ @Override
+ public void update(DocumentRecord record) {}
+ @Override
+ public void deleteByFingerprint(DocumentFingerprint fingerprint) {}
+ };
+
+ var useCase = new DefaultResolveHistoricalDocumentContextUseCase(throwingRepo);
+
+ assertThat(useCase.resolveHistoricalDocumentContext(FP)).isEmpty();
+ }
+
+ // -------------------------------------------------------------------------
+ // Hilfsmethoden
+ // -------------------------------------------------------------------------
+
+ private static DocumentRecordRepository stubRepo(DocumentRecordLookupResult result) {
+ return new DocumentRecordRepository() {
+ @Override
+ public DocumentRecordLookupResult findByFingerprint(DocumentFingerprint fingerprint) {
+ return result;
+ }
+ @Override
+ public void create(DocumentRecord record) {}
+ @Override
+ public void update(DocumentRecord record) {}
+ @Override
+ public void deleteByFingerprint(DocumentFingerprint fingerprint) {}
+ };
+ }
+
+ private static DocumentRecord buildRecord(
+ ProcessingStatus status,
+ String lastTargetFileName,
+ Instant lastSuccessInstant,
+ Instant lastFailureInstant) {
+ return new DocumentRecord(
+ FP,
+ new SourceDocumentLocator("quell/pfad"),
+ "original.pdf",
+ status,
+ FailureCounters.zero(),
+ lastFailureInstant,
+ lastSuccessInstant,
+ Instant.now(),
+ Instant.now(),
+ lastTargetFileName != null ? "ziel/ordner" : null,
+ lastTargetFileName);
+ }
+}
diff --git a/pdf-umbenenner-bootstrap/src/main/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunner.java b/pdf-umbenenner-bootstrap/src/main/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunner.java
index 5955ace..8a4f902 100644
--- a/pdf-umbenenner-bootstrap/src/main/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunner.java
+++ b/pdf-umbenenner-bootstrap/src/main/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunner.java
@@ -24,7 +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.GuiHistoricalDocumentContextPort;
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;
@@ -59,8 +59,9 @@ 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.HistoricalDocumentContext;
+import de.gecheckt.pdf.umbenenner.application.port.in.ResolveHistoricalDocumentContextUseCase;
+import de.gecheckt.pdf.umbenenner.application.usecase.DefaultResolveHistoricalDocumentContextUseCase;
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;
@@ -686,7 +687,7 @@ public class BootstrapRunner {
de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun.GuiResetDocumentStatusPort resetPort =
this::resetDocumentStatusForGui;
GuiManualFileRenamePort manualRenamePort = this::performGuiManualFileRename;
- GuiHistoricalFileNamePort historicalFileNamePort = this::resolveHistoricalFileNameForGui;
+ GuiHistoricalDocumentContextPort historicalDocumentContextPort = this::resolveHistoricalDocumentContextForGui;
if (configPathOverride.isEmpty()) {
return new GuiStartupContext(
@@ -704,7 +705,7 @@ public class BootstrapRunner {
miniRunLauncher,
resetPort,
manualRenamePort,
- historicalFileNamePort);
+ historicalDocumentContextPort);
}
Path configPath = Paths.get(configPathOverride.get());
@@ -727,7 +728,7 @@ public class BootstrapRunner {
miniRunLauncher,
resetPort,
manualRenamePort,
- historicalFileNamePort);
+ historicalDocumentContextPort);
}
LOG.info("GUI startup: configuration file confirmed at: {}", configPath.toAbsolutePath());
@@ -736,7 +737,7 @@ public class BootstrapRunner {
return new GuiStartupContext(loadedState, Optional.empty(), loader, writer,
modelCatalogPort, apiKeyResolutionPort, providerTechnicalTestService, pathCheckPort,
technicalTestOrchestrator, correctionExecutionService, batchRunLauncher,
- miniRunLauncher, resetPort, manualRenamePort, historicalFileNamePort);
+ miniRunLauncher, resetPort, manualRenamePort, historicalDocumentContextPort);
} catch (GuiConfigurationLoadException e) {
LOG.error("GUI startup: configuration could not be loaded, starting without it: {}",
e.getMessage(), e);
@@ -755,7 +756,7 @@ public class BootstrapRunner {
miniRunLauncher,
resetPort,
manualRenamePort,
- historicalFileNamePort);
+ historicalDocumentContextPort);
}
}
@@ -1070,28 +1071,28 @@ public class BootstrapRunner {
}
/**
- * Resolves the historical AI-proposed target filename for a document identified by
+ * Resolves the historical processing context for the document identified by
* {@code fingerprint}, using the configuration at {@code configFilePath}.
*
* 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.
+ * {@link ResolveHistoricalDocumentContextUseCase}. Technical errors during loading or
+ * querying are caught and returned as an empty {@link Optional}; they are never propagated
+ * to the caller.
*
* 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
+ * @return the historical processing context, or empty if not available
*/
- Optional resolveHistoricalFileNameForGui(
+ Optional resolveHistoricalDocumentContextForGui(
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);
+ LOG.debug("Historischer Kontext: Konfigurationsdatei nicht gefunden: {}", configFilePath);
return Optional.empty();
}
@@ -1102,11 +1103,11 @@ public class BootstrapRunner {
String jdbcUrl = buildJdbcUrl(config);
DocumentRecordRepository documentRecordRepository =
new SqliteDocumentRecordRepositoryAdapter(jdbcUrl);
- ResolveHistoricalFileNameUseCase useCase =
- new DefaultResolveHistoricalFileNameUseCase(documentRecordRepository);
- return useCase.resolveHistoricalFileName(fingerprint);
+ ResolveHistoricalDocumentContextUseCase useCase =
+ new DefaultResolveHistoricalDocumentContextUseCase(documentRecordRepository);
+ return useCase.resolveHistoricalDocumentContext(fingerprint);
} catch (Exception e) {
- LOG.debug("Historischer Dateiname konnte nicht abgefragt werden für {}: {}",
+ LOG.debug("Historischer Kontext konnte nicht abgefragt werden für {}: {}",
fingerprint.sha256Hex(), e.getMessage());
return Optional.empty();
}