#7: Historien-Tab mit Liste, Detail, Filter, Status-Reset und Eintrag-Loeschen
Implementiert den vollstaendigen Historien-Tab (Verlauf) als vierten Tab der GUI. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+23
@@ -60,5 +60,28 @@ public interface UnitOfWorkPort {
|
||||
* @throws DocumentPersistenceException if the delete fails due to a technical error
|
||||
*/
|
||||
void resetDocumentByFingerprint(DocumentFingerprint fingerprint);
|
||||
|
||||
/**
|
||||
* Setzt ausschließlich die vier fachlich relevanten Status-Felder zurück,
|
||||
* ohne die Versuchshistorie zu löschen.
|
||||
* <p>
|
||||
* Folgende Felder werden aktualisiert:
|
||||
* <ul>
|
||||
* <li>{@code overall_status} → {@code READY_FOR_AI}</li>
|
||||
* <li>{@code content_error_count} → {@code 0}</li>
|
||||
* <li>{@code transient_error_count} → {@code 0}</li>
|
||||
* <li>{@code last_failure_instant} → {@code null}</li>
|
||||
* </ul>
|
||||
* Nicht geändert werden: {@code created_at}, {@code last_success_instant},
|
||||
* {@code last_target_path}, {@code last_target_file_name} sowie alle
|
||||
* {@code processing_attempt}-Einträge, die vollständig erhalten bleiben.
|
||||
* <p>
|
||||
* Nach diesem Aufruf gilt das Dokument beim nächsten Lauf als verarbeitbar.
|
||||
*
|
||||
* @param fingerprint der Dokumentbezeichner, dessen Status zurückgesetzt werden soll;
|
||||
* darf nicht {@code null} sein
|
||||
* @throws DocumentPersistenceException bei technischen Datenbankfehlern
|
||||
*/
|
||||
void resetDocumentStatusForRetry(DocumentFingerprint fingerprint);
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.port.out.history;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Objects;
|
||||
|
||||
import de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint;
|
||||
import de.gecheckt.pdf.umbenenner.domain.model.ProcessingStatus;
|
||||
|
||||
/**
|
||||
* Einzelzeile der Dokumentenliste im Historien-Tab.
|
||||
* <p>
|
||||
* Enthält alle Felder, die für die linke Tabelle des Historien-Tabs benötigt werden.
|
||||
* Die Felder stammen aus {@code document_record} und einem {@code COUNT}-Ausdruck über
|
||||
* {@code processing_attempt}.
|
||||
*
|
||||
* @param fingerprint Inhalts-basierter Dokumentbezeichner; nie {@code null}
|
||||
* @param overallStatus aktueller Gesamtstatus des Dokuments; nie {@code null}
|
||||
* @param sourceFileName zuletzt bekannter Quelldateiname; nie {@code null}
|
||||
* @param targetFileName zuletzt bekannter Zieldateiname; {@code null} falls noch kein
|
||||
* erfolgreicher Lauf stattgefunden hat
|
||||
* @param sourcePath zuletzt bekannter Quellpfad (opaker Locator-Wert); nie {@code null}
|
||||
* @param updatedAt Zeitpunkt der letzten Aktualisierung des Stammsatzes; nie {@code null}
|
||||
* @param attemptCount Anzahl historisierter Verarbeitungsversuche; immer >= 0
|
||||
*/
|
||||
public record DocumentHistoryRow(
|
||||
DocumentFingerprint fingerprint,
|
||||
ProcessingStatus overallStatus,
|
||||
String sourceFileName,
|
||||
String targetFileName,
|
||||
String sourcePath,
|
||||
Instant updatedAt,
|
||||
long attemptCount) {
|
||||
|
||||
/**
|
||||
* Kompakter Konstruktor mit Pflichtfeldprüfung.
|
||||
*
|
||||
* @throws NullPointerException wenn ein Pflichtfeld {@code null} ist
|
||||
* @throws IllegalArgumentException wenn {@code attemptCount} negativ ist
|
||||
*/
|
||||
public DocumentHistoryRow {
|
||||
Objects.requireNonNull(fingerprint, "fingerprint darf nicht null sein");
|
||||
Objects.requireNonNull(overallStatus, "overallStatus darf nicht null sein");
|
||||
Objects.requireNonNull(sourceFileName, "sourceFileName darf nicht null sein");
|
||||
Objects.requireNonNull(sourcePath, "sourcePath darf nicht null sein");
|
||||
Objects.requireNonNull(updatedAt, "updatedAt darf nicht null sein");
|
||||
if (attemptCount < 0) {
|
||||
throw new IllegalArgumentException("attemptCount darf nicht negativ sein, war: " + attemptCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.port.out.history;
|
||||
|
||||
/**
|
||||
* Abfrageparameter für den Historien-Tab.
|
||||
* <p>
|
||||
* Kapselt Freitextsuche, optionalen Status-Filter und das Limit der zurückzugebenden
|
||||
* Zeilen. Das Limit ist bewusst auf 501 gesetzt, damit die aufrufende Schicht erkennen
|
||||
* kann, ob mehr als 500 Treffer vorhanden sind.
|
||||
*
|
||||
* @param searchText optionaler Suchbegriff (Teilstring, case-insensitiv); {@code null}
|
||||
* oder leer bedeutet keine Texteinschränkung
|
||||
* @param statusFilter optionaler Status-Filter als Enum-Name; {@code null} bedeutet alle
|
||||
* Status werden angezeigt
|
||||
* @param limit maximale Anzahl zurückzugebender Zeilen; muss >= 1 sein
|
||||
*/
|
||||
public record HistoryQuery(
|
||||
String searchText,
|
||||
String statusFilter,
|
||||
int limit) {
|
||||
|
||||
/**
|
||||
* Standard-Limit: 501 Zeilen abfragen, um bei Bedarf „mehr vorhanden" erkennen zu können.
|
||||
*/
|
||||
public static final int DEFAULT_LIMIT = 501;
|
||||
|
||||
/**
|
||||
* Kompakter Konstruktor mit Pflichtfeldprüfung.
|
||||
*
|
||||
* @throws IllegalArgumentException wenn {@code limit} kleiner als 1 ist
|
||||
*/
|
||||
public HistoryQuery {
|
||||
if (limit < 1) {
|
||||
throw new IllegalArgumentException("limit muss mindestens 1 sein, war: " + limit);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Erzeugt eine Abfrage ohne Filter mit Standard-Limit.
|
||||
*
|
||||
* @return neue Abfrage ohne Einschränkungen
|
||||
*/
|
||||
public static HistoryQuery unfiltered() {
|
||||
return new HistoryQuery(null, null, DEFAULT_LIMIT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Erzeugt eine Abfrage mit Freitextsuche und Standard-Limit.
|
||||
*
|
||||
* @param searchText Suchbegriff; {@code null} oder leer bedeutet kein Filter
|
||||
* @return neue Abfrage mit Textfilter
|
||||
*/
|
||||
public static HistoryQuery withSearchText(String searchText) {
|
||||
return new HistoryQuery(searchText, null, DEFAULT_LIMIT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Erzeugt eine Abfrage mit Status-Filter und Standard-Limit.
|
||||
*
|
||||
* @param statusFilter Enum-Name des gewünschten Status; {@code null} bedeutet kein Filter
|
||||
* @return neue Abfrage mit Status-Filter
|
||||
*/
|
||||
public static HistoryQuery withStatus(String statusFilter) {
|
||||
return new HistoryQuery(null, statusFilter, DEFAULT_LIMIT);
|
||||
}
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.port.out.history;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.DocumentRecord;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.ProcessingAttempt;
|
||||
import de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint;
|
||||
|
||||
/**
|
||||
* Outbound-Port für lesende Historien-Abfragen aus dem Historien-Tab.
|
||||
* <p>
|
||||
* Kapselt alle Datenbanklese-Operationen, die der Historien-Tab benötigt.
|
||||
* Die Implementierung liegt ausschließlich in {@code pdf-umbenenner-adapter-out}.
|
||||
* Die Application-Schicht kennt nur diesen Port-Vertrag – keine JDBC-Typen.
|
||||
*
|
||||
* <h2>Architektur</h2>
|
||||
* <p>
|
||||
* Dieser Port ist bewusst von {@link de.gecheckt.pdf.umbenenner.application.port.out.DocumentRecordRepository}
|
||||
* und {@link de.gecheckt.pdf.umbenenner.application.port.out.ProcessingAttemptRepository}
|
||||
* getrennt, damit die bestehenden Repositories nicht mit GUI-spezifischen Methoden
|
||||
* aufgebläht werden.
|
||||
*/
|
||||
public interface HistoryQueryPort {
|
||||
|
||||
/**
|
||||
* Lädt eine gefilterte und sortierte Übersicht aller Dokumenteneinträge.
|
||||
* <p>
|
||||
* Sortierung: {@code updated_at DESC, fingerprint ASC} (stabiler Tie-Breaker).
|
||||
* Das in {@link HistoryQuery#limit()} angegebene Limit wird direkt als SQL-{@code LIMIT}
|
||||
* angewendet. Wenn das Limit 501 beträgt und 501 Zeilen zurückgegeben werden, gibt es
|
||||
* mehr als 500 Treffer.
|
||||
*
|
||||
* @param query Abfrageparameter mit Suchtext, Status-Filter und Limit; darf nicht {@code null} sein
|
||||
* @return unveränderliche Liste der Trefferzeilen; nie {@code null}; kann leer sein
|
||||
* @throws de.gecheckt.pdf.umbenenner.application.port.out.DocumentPersistenceException bei
|
||||
* technischen Datenbankfehlern
|
||||
*/
|
||||
List<DocumentHistoryRow> loadOverview(HistoryQuery query);
|
||||
|
||||
/**
|
||||
* Lädt den vollständigen Dokumenten-Stammsatz für den angegebenen Fingerprint.
|
||||
*
|
||||
* @param fingerprint Dokumentbezeichner; darf nicht {@code null} sein
|
||||
* @return Optional mit dem Stammsatz, oder leer wenn nicht vorhanden
|
||||
* @throws de.gecheckt.pdf.umbenenner.application.port.out.DocumentPersistenceException bei
|
||||
* technischen Datenbankfehlern
|
||||
*/
|
||||
Optional<DocumentRecord> findRecordByFingerprint(DocumentFingerprint fingerprint);
|
||||
|
||||
/**
|
||||
* Lädt alle historisierten Verarbeitungsversuche für den angegebenen Fingerprint,
|
||||
* aufsteigend sortiert nach {@code attempt_number}.
|
||||
*
|
||||
* @param fingerprint Dokumentbezeichner; darf nicht {@code null} sein
|
||||
* @return unveränderliche Liste der Versuche; nie {@code null}; kann leer sein
|
||||
* @throws de.gecheckt.pdf.umbenenner.application.port.out.DocumentPersistenceException bei
|
||||
* technischen Datenbankfehlern
|
||||
*/
|
||||
List<ProcessingAttempt> findAttemptsByFingerprint(DocumentFingerprint fingerprint);
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Outbound-Ports und DTOs für lesende Historien-Abfragen des Historien-Tabs.
|
||||
* <p>
|
||||
* Enthält den {@link de.gecheckt.pdf.umbenenner.application.port.out.history.HistoryQueryPort}
|
||||
* sowie die zugehörigen Datentypen
|
||||
* {@link de.gecheckt.pdf.umbenenner.application.port.out.history.HistoryQuery} und
|
||||
* {@link de.gecheckt.pdf.umbenenner.application.port.out.history.DocumentHistoryRow}.
|
||||
* Diese Typen sind bewusst vom bestehenden {@code port.out}-Paket getrennt,
|
||||
* damit die allgemeinen Repository-Schnittstellen nicht mit GUI-spezifischen Methoden
|
||||
* belastet werden.
|
||||
*/
|
||||
package de.gecheckt.pdf.umbenenner.application.port.out.history;
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.usecase;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.DocumentPersistenceException;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.UnitOfWorkPort;
|
||||
import de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint;
|
||||
|
||||
/**
|
||||
* Use-Case-Implementierung für das vollständige Löschen eines Dokumenteintrags
|
||||
* aus dem Historien-Tab.
|
||||
* <p>
|
||||
* Löscht innerhalb einer Transaktion in der korrekten Reihenfolge, um den
|
||||
* Foreign-Key-Constraint zwischen {@code processing_attempt.fingerprint} und
|
||||
* {@code document_record.fingerprint} zu erfüllen (kein {@code ON DELETE CASCADE}):
|
||||
* <ol>
|
||||
* <li>Alle {@code processing_attempt}-Einträge zum Fingerprint</li>
|
||||
* <li>Den {@code document_record}-Stammsatz zum Fingerprint</li>
|
||||
* </ol>
|
||||
* Die Operation ist idempotent: wenn kein Datensatz für den Fingerprint existiert,
|
||||
* kehrt die Methode stillschweigend zurück.
|
||||
* <p>
|
||||
* <strong>Hinweis:</strong> Diese Aktion ist destruktiv und nicht rückgängig zu machen.
|
||||
* Die GUI muss vor dem Aufruf einen Bestätigungsdialog anzeigen.
|
||||
*/
|
||||
public class DefaultDeleteDocumentHistoryUseCase {
|
||||
|
||||
private static final Logger logger = LogManager.getLogger(DefaultDeleteDocumentHistoryUseCase.class);
|
||||
|
||||
private final UnitOfWorkPort unitOfWorkPort;
|
||||
|
||||
/**
|
||||
* Erzeugt den Use-Case mit dem erforderlichen Persistenz-Port.
|
||||
*
|
||||
* @param unitOfWorkPort Port für transaktionale Persistenzoperationen; darf nicht {@code null} sein
|
||||
* @throws NullPointerException wenn {@code unitOfWorkPort} null ist
|
||||
*/
|
||||
public DefaultDeleteDocumentHistoryUseCase(UnitOfWorkPort unitOfWorkPort) {
|
||||
this.unitOfWorkPort = Objects.requireNonNull(unitOfWorkPort, "unitOfWorkPort darf nicht null sein");
|
||||
}
|
||||
|
||||
/**
|
||||
* Löscht den Stammsatz und alle Verarbeitungsversuche für den angegebenen Fingerprint.
|
||||
* <p>
|
||||
* Die Löschung erfolgt in einer einzigen Transaktion. Versuche werden vor dem
|
||||
* Stammsatz gelöscht, damit der Foreign-Key-Constraint eingehalten wird.
|
||||
*
|
||||
* @param fingerprint der Dokumentbezeichner, dessen Daten vollständig gelöscht werden sollen;
|
||||
* darf nicht {@code null} sein
|
||||
* @throws DocumentPersistenceException bei technischen Datenbankfehlern
|
||||
* @throws NullPointerException wenn {@code fingerprint} null ist
|
||||
*/
|
||||
public void deleteHistory(DocumentFingerprint fingerprint) {
|
||||
Objects.requireNonNull(fingerprint, "fingerprint darf nicht null sein");
|
||||
|
||||
// Nutzung der bestehenden Transaktion mit korrekter Löschreihenfolge:
|
||||
// zuerst Versuche, dann Stammsatz (FK-Constraint)
|
||||
unitOfWorkPort.executeInTransaction(tx -> tx.resetDocumentByFingerprint(fingerprint));
|
||||
|
||||
logger.info("Dokumenteintrag vollständig gelöscht für Fingerprint: {}", fingerprint.sha256Hex());
|
||||
}
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.usecase;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.DocumentRecord;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.ProcessingAttempt;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.history.HistoryQueryPort;
|
||||
import de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint;
|
||||
|
||||
/**
|
||||
* Use-Case-Implementierung für das Laden der Detailansicht eines Dokuments im Historien-Tab.
|
||||
* <p>
|
||||
* Kombiniert den Dokument-Stammsatz und alle historisierten Verarbeitungsversuche
|
||||
* für einen bestimmten Fingerprint in einem einzigen Ergebnisobjekt.
|
||||
* <p>
|
||||
* Wird kein Stammsatz gefunden (z. B. weil das Dokument zwischenzeitlich gelöscht wurde),
|
||||
* liefert {@link #loadDetails(DocumentFingerprint)} ein leeres {@link Optional}.
|
||||
*/
|
||||
public class DefaultHistoryDetailsUseCase {
|
||||
|
||||
private final HistoryQueryPort historyQueryPort;
|
||||
|
||||
/**
|
||||
* Erzeugt den Use-Case mit dem erforderlichen Abfrage-Port.
|
||||
*
|
||||
* @param historyQueryPort Port für lesende Historienabfragen; darf nicht {@code null} sein
|
||||
* @throws NullPointerException wenn {@code historyQueryPort} null ist
|
||||
*/
|
||||
public DefaultHistoryDetailsUseCase(HistoryQueryPort historyQueryPort) {
|
||||
this.historyQueryPort = Objects.requireNonNull(historyQueryPort, "historyQueryPort darf nicht null sein");
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt den Stammsatz und alle Verarbeitungsversuche für den angegebenen Fingerprint.
|
||||
*
|
||||
* @param fingerprint Dokumentbezeichner; darf nicht {@code null} sein
|
||||
* @return Optional mit den Detaildaten, oder leer wenn kein Stammsatz gefunden wurde
|
||||
* @throws de.gecheckt.pdf.umbenenner.application.port.out.DocumentPersistenceException
|
||||
* bei technischen Datenbankfehlern
|
||||
*/
|
||||
public Optional<HistoryDetailsResult> loadDetails(DocumentFingerprint fingerprint) {
|
||||
Objects.requireNonNull(fingerprint, "fingerprint darf nicht null sein");
|
||||
|
||||
Optional<DocumentRecord> record = historyQueryPort.findRecordByFingerprint(fingerprint);
|
||||
if (record.isEmpty()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
List<ProcessingAttempt> attempts = historyQueryPort.findAttemptsByFingerprint(fingerprint);
|
||||
return Optional.of(new HistoryDetailsResult(record.get(), attempts));
|
||||
}
|
||||
|
||||
/**
|
||||
* Ergebnis einer Historien-Detailabfrage.
|
||||
*
|
||||
* @param record Dokument-Stammsatz; nie {@code null}
|
||||
* @param attempts alle historisierten Verarbeitungsversuche aufsteigend nach Versuchsnummer;
|
||||
* nie {@code null}; kann leer sein
|
||||
*/
|
||||
public record HistoryDetailsResult(DocumentRecord record, List<ProcessingAttempt> attempts) {
|
||||
|
||||
/**
|
||||
* Kompakter Konstruktor mit Pflichtfeldprüfung.
|
||||
*
|
||||
* @throws NullPointerException wenn {@code record} oder {@code attempts} null ist
|
||||
*/
|
||||
public HistoryDetailsResult {
|
||||
Objects.requireNonNull(record, "record darf nicht null sein");
|
||||
Objects.requireNonNull(attempts, "attempts darf nicht null sein");
|
||||
}
|
||||
}
|
||||
}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.usecase;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.history.DocumentHistoryRow;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.history.HistoryQuery;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.history.HistoryQueryPort;
|
||||
|
||||
/**
|
||||
* Use-Case-Implementierung für das Laden der Dokumentenliste im Historien-Tab.
|
||||
* <p>
|
||||
* Delegiert die Datenbankabfrage vollständig an {@link HistoryQueryPort} und
|
||||
* wertet das LIMIT-501-Ergebnis aus, um der GUI signalisieren zu können, ob
|
||||
* weitere Einträge vorhanden sind, die durch einen engeren Filter erreichbar wären.
|
||||
* <p>
|
||||
* <strong>LIMIT-501-Technik:</strong> Die Query wird mit {@code limit + 1 = 501}
|
||||
* ausgeführt (sofern das übergebene Limit 500 beträgt). Wenn die Datenbank 501
|
||||
* Zeilen zurückgibt, existieren mehr als 500 Treffer. Die zurückgegebene Liste
|
||||
* enthält dann exakt 500 Zeilen (das letzte Element wird verworfen) und
|
||||
* {@link HistoryOverviewResult#hasMore()} liefert {@code true}.
|
||||
*/
|
||||
public class DefaultHistoryOverviewUseCase {
|
||||
|
||||
private static final int MAX_DISPLAY_COUNT = 500;
|
||||
|
||||
private final HistoryQueryPort historyQueryPort;
|
||||
|
||||
/**
|
||||
* Erzeugt den Use-Case mit dem erforderlichen Abfrage-Port.
|
||||
*
|
||||
* @param historyQueryPort Port für lesende Historienabfragen; darf nicht {@code null} sein
|
||||
* @throws NullPointerException wenn {@code historyQueryPort} null ist
|
||||
*/
|
||||
public DefaultHistoryOverviewUseCase(HistoryQueryPort historyQueryPort) {
|
||||
this.historyQueryPort = Objects.requireNonNull(historyQueryPort, "historyQueryPort darf nicht null sein");
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt die Dokumentenliste auf Basis der übergebenen Abfrageparameter.
|
||||
* <p>
|
||||
* Intern wird ein Limit von 501 verwendet, um erkennen zu können, ob mehr
|
||||
* als 500 Treffer vorhanden sind.
|
||||
*
|
||||
* @param query Abfrageparameter mit Suchtext, Status-Filter und Limit; darf nicht {@code null} sein
|
||||
* @return Ergebnisobjekt mit Trefferlist und {@code hasMore}-Flag; nie {@code null}
|
||||
* @throws de.gecheckt.pdf.umbenenner.application.port.out.DocumentPersistenceException
|
||||
* bei technischen Datenbankfehlern
|
||||
*/
|
||||
public HistoryOverviewResult loadOverview(HistoryQuery query) {
|
||||
Objects.requireNonNull(query, "query darf nicht null sein");
|
||||
|
||||
List<DocumentHistoryRow> rows = historyQueryPort.loadOverview(query);
|
||||
|
||||
if (rows.size() > MAX_DISPLAY_COUNT) {
|
||||
// 501 Zeilen zurückgegeben: mehr als 500 Treffer vorhanden
|
||||
List<DocumentHistoryRow> truncated = List.copyOf(rows.subList(0, MAX_DISPLAY_COUNT));
|
||||
return new HistoryOverviewResult(truncated, true);
|
||||
}
|
||||
|
||||
return new HistoryOverviewResult(List.copyOf(rows), false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ergebnis einer Historien-Übersichtsabfrage.
|
||||
*
|
||||
* @param rows Liste der Trefferzeilen; nie {@code null}; enthält maximal 500 Einträge
|
||||
* @param hasMore {@code true}, wenn mehr als 500 Treffer vorhanden sind und durch
|
||||
* einen engeren Filter eingegrenzt werden könnten
|
||||
*/
|
||||
public record HistoryOverviewResult(List<DocumentHistoryRow> rows, boolean hasMore) {
|
||||
|
||||
/**
|
||||
* Kompakter Konstruktor mit Pflichtfeldprüfung.
|
||||
*
|
||||
* @throws NullPointerException wenn {@code rows} null ist
|
||||
*/
|
||||
public HistoryOverviewResult {
|
||||
Objects.requireNonNull(rows, "rows darf nicht null sein");
|
||||
}
|
||||
}
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.usecase;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.DocumentPersistenceException;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.UnitOfWorkPort;
|
||||
import de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint;
|
||||
|
||||
/**
|
||||
* Use-Case-Implementierung für den feldgenauen Status-Reset aus dem Historien-Tab.
|
||||
* <p>
|
||||
* Setzt ausschließlich die vier fachlich relevanten Status-Felder zurück,
|
||||
* ohne die Versuchshistorie zu löschen:
|
||||
* <ul>
|
||||
* <li>{@code overall_status} → {@code READY_FOR_AI}</li>
|
||||
* <li>{@code content_error_count} → {@code 0}</li>
|
||||
* <li>{@code transient_error_count} → {@code 0}</li>
|
||||
* <li>{@code last_failure_instant} → {@code null}</li>
|
||||
* </ul>
|
||||
* Nicht geändert werden: {@code created_at}, {@code last_success_instant},
|
||||
* {@code last_target_path}, {@code last_target_file_name} sowie alle
|
||||
* {@code processing_attempt}-Einträge, die vollständig erhalten bleiben.
|
||||
* <p>
|
||||
* Nach dem Reset gilt das Dokument beim nächsten Verarbeitungslauf als verarbeitbar,
|
||||
* da {@code READY_FOR_AI} der einzige Trigger für die Verarbeitungslogik ist.
|
||||
* <p>
|
||||
* <strong>Abgrenzung:</strong> Dieser Use-Case unterscheidet sich von
|
||||
* {@link DefaultResetDocumentStatusUseCase}, der alle Persistenzdaten (Stammsatz und
|
||||
* Versuchshistorie) vollständig löscht und das Dokument so behandelt, als wäre es
|
||||
* noch nie verarbeitet worden.
|
||||
*/
|
||||
public class DefaultHistoryResetDocumentStatusUseCase {
|
||||
|
||||
private static final Logger logger = LogManager.getLogger(DefaultHistoryResetDocumentStatusUseCase.class);
|
||||
|
||||
private final UnitOfWorkPort unitOfWorkPort;
|
||||
|
||||
/**
|
||||
* Erzeugt den Use-Case mit dem erforderlichen Persistenz-Port.
|
||||
*
|
||||
* @param unitOfWorkPort Port für transaktionale Persistenzoperationen; darf nicht {@code null} sein
|
||||
* @throws NullPointerException wenn {@code unitOfWorkPort} null ist
|
||||
*/
|
||||
public DefaultHistoryResetDocumentStatusUseCase(UnitOfWorkPort unitOfWorkPort) {
|
||||
this.unitOfWorkPort = Objects.requireNonNull(unitOfWorkPort, "unitOfWorkPort darf nicht null sein");
|
||||
}
|
||||
|
||||
/**
|
||||
* Führt den feldgenauen Status-Reset für den angegebenen Fingerprint durch.
|
||||
* <p>
|
||||
* Die Operation ist atomar: entweder werden alle vier Felder aktualisiert,
|
||||
* oder keine Änderung findet statt (Rollback).
|
||||
*
|
||||
* @param fingerprint der Dokumentbezeichner, dessen Status zurückgesetzt werden soll;
|
||||
* darf nicht {@code null} sein
|
||||
* @throws DocumentPersistenceException bei technischen Datenbankfehlern
|
||||
* @throws NullPointerException wenn {@code fingerprint} null ist
|
||||
*/
|
||||
public void resetStatus(DocumentFingerprint fingerprint) {
|
||||
Objects.requireNonNull(fingerprint, "fingerprint darf nicht null sein");
|
||||
|
||||
unitOfWorkPort.executeInTransaction(tx -> tx.resetDocumentStatusForRetry(fingerprint));
|
||||
|
||||
logger.info("Feldgenauer Status-Reset durchgeführt für Fingerprint: {}", fingerprint.sha256Hex());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user