From 3f5602de01508ce366cf27d2d1aabc66dbcc5c50 Mon Sep 17 00:00:00 2001 From: Marcus van Elst Date: Mon, 27 Apr 2026 12:00:27 +0200 Subject: [PATCH] =?UTF-8?q?Fix=20#30:=20Detailbereich=20bei=20SKIPPED-Zeil?= =?UTF-8?q?en=20mit=20historischen=20Informationen=20bef=C3=BCllen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Teile DocumentCompletionStatus.SKIPPED in SKIPPED_ALREADY_PROCESSED und SKIPPED_FINAL_FAILURE auf, um den Skip-Grund unterscheidbar zu machen - Führe neuen Typ HistoricalDocumentContext ein (lastTargetFileName, lastSuccessInstant, lastFailureInstant, wasEverSuccessful) - Führe ResolveHistoricalDocumentContextUseCase und DefaultResolveHistoricalDocumentContextUseCase ein - Ersetze GuiHistoricalFileNamePort durch GuiHistoricalDocumentContextPort - Lade historischen Kontext für übersprungene Zeilen im Coordinator-Worker-Thread - Zeige im Detailbereich je nach Skip-Grund: SKIPPED_ALREADY_PROCESSED: "Bereits erfolgreich verarbeitet am [Datum]. Zieldatei: [Name]." SKIPPED_FINAL_FAILURE: "Endgültig fehlgeschlagen am [Datum]. Erneute Verarbeitung nur nach Reset möglich." - Passe alle betroffenen Tests an Co-Authored-By: Claude Sonnet 4.6 --- .../gui/GuiConfigurationEditorWorkspace.java | 12 +- .../adapter/in/gui/GuiStartupContext.java | 26 +-- .../in/gui/batchrun/FileNameEditorPane.java | 2 +- .../gui/batchrun/GuiBatchRunCoordinator.java | 66 ++++--- .../in/gui/batchrun/GuiBatchRunResultRow.java | 27 ++- .../in/gui/batchrun/GuiBatchRunTab.java | 54 +++++- .../GuiHistoricalDocumentContextPort.java | 42 ++++ .../gui/batchrun/FileNameEditorPaneTest.java | 2 +- .../batchrun/GuiBatchRunCoordinatorTest.java | 6 +- .../batchrun/GuiBatchRunResultRowTest.java | 9 +- .../GuiBatchRunTabSelectionSmokeTest.java | 2 +- .../gui/batchrun/GuiBatchRunTabSmokeTest.java | 4 +- .../port/in/DocumentCompletionStatus.java | 20 +- .../port/in/HistoricalDocumentContext.java | 75 ++++++++ ...solveHistoricalDocumentContextUseCase.java | 42 ++++ .../application/port/in/RunSummary.java | 5 +- .../DocumentProcessingCoordinator.java | 6 +- .../usecase/CountingCompletionObserver.java | 2 +- ...solveHistoricalDocumentContextUseCase.java | 83 ++++++++ .../BatchRunProgressObservationTest.java | 4 +- ...eHistoricalDocumentContextUseCaseTest.java | 180 ++++++++++++++++++ .../umbenenner/bootstrap/BootstrapRunner.java | 39 ++-- 22 files changed, 605 insertions(+), 103 deletions(-) create mode 100644 pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/GuiHistoricalDocumentContextPort.java create mode 100644 pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/in/HistoricalDocumentContext.java create mode 100644 pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/port/in/ResolveHistoricalDocumentContextUseCase.java create mode 100644 pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/usecase/DefaultResolveHistoricalDocumentContextUseCase.java create mode 100644 pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/usecase/DefaultResolveHistoricalDocumentContextUseCaseTest.java 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: + *

+ * 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(); }