#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:
2026-04-30 13:57:07 +02:00
parent 5d5dee0bbf
commit 46fc1d4fa4
31 changed files with 3095 additions and 17 deletions
@@ -87,7 +87,18 @@ import de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.AiModelCatal
import de.gecheckt.pdf.umbenenner.application.service.AiNamingService;
import de.gecheckt.pdf.umbenenner.application.service.AiResponseValidator;
import de.gecheckt.pdf.umbenenner.application.service.DocumentProcessingCoordinator;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.history.GuiDeleteDocumentHistoryPort;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.history.GuiHistoryDetailsPort;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.history.GuiHistoryOverviewPort;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.history.GuiHistoryResetDocumentStatusPort;
import de.gecheckt.pdf.umbenenner.adapter.out.sqlite.SqliteHistoryQueryAdapter;
import de.gecheckt.pdf.umbenenner.application.port.out.history.HistoryQuery;
import de.gecheckt.pdf.umbenenner.application.port.out.history.HistoryQueryPort;
import de.gecheckt.pdf.umbenenner.application.usecase.DefaultBatchRunProcessingUseCase;
import de.gecheckt.pdf.umbenenner.application.usecase.DefaultDeleteDocumentHistoryUseCase;
import de.gecheckt.pdf.umbenenner.application.usecase.DefaultHistoryDetailsUseCase;
import de.gecheckt.pdf.umbenenner.application.usecase.DefaultHistoryOverviewUseCase;
import de.gecheckt.pdf.umbenenner.application.usecase.DefaultHistoryResetDocumentStatusUseCase;
import de.gecheckt.pdf.umbenenner.application.usecase.DefaultManualFileCopyUseCase;
import de.gecheckt.pdf.umbenenner.application.usecase.DefaultManualFileRenameUseCase;
import de.gecheckt.pdf.umbenenner.application.usecase.DefaultPromptEditorUseCase;
@@ -808,6 +819,10 @@ public class BootstrapRunner {
GuiManualFileRenamePort manualRenamePort = this::performGuiManualFileRename;
GuiManualFileCopyPort manualCopyPort = this::performGuiManualFileCopy;
GuiHistoricalDocumentContextPort historicalDocumentContextPort = this::resolveHistoricalDocumentContextForGui;
GuiHistoryOverviewPort historyOverviewPort = this::loadHistoryOverviewForGui;
GuiHistoryDetailsPort historyDetailsPort = this::loadHistoryDetailsForGui;
GuiHistoryResetDocumentStatusPort historyResetPort = this::resetHistoryDocumentStatusForGui;
GuiDeleteDocumentHistoryPort deleteHistoryPort = this::deleteDocumentHistoryForGui;
// Versionsnummer aus dem MANIFEST.MF des gepackten JARs lesen; Fallback "dev" bei IDE-Start
String applicationVersion = ApplicationVersionProvider.resolveVersion();
@@ -830,7 +845,11 @@ public class BootstrapRunner {
manualCopyPort,
historicalDocumentContextPort,
applicationVersion,
noOpGuiPromptEditorPort());
noOpGuiPromptEditorPort(),
historyOverviewPort,
historyDetailsPort,
historyResetPort,
deleteHistoryPort);
}
Path configPath = Paths.get(configPathOverride.get());
@@ -856,7 +875,11 @@ public class BootstrapRunner {
manualCopyPort,
historicalDocumentContextPort,
applicationVersion,
noOpGuiPromptEditorPort());
noOpGuiPromptEditorPort(),
historyOverviewPort,
historyDetailsPort,
historyResetPort,
deleteHistoryPort);
}
LOG.info("GUI startup: configuration file confirmed at: {}", configPath.toAbsolutePath());
@@ -868,7 +891,8 @@ public class BootstrapRunner {
modelCatalogPort, apiKeyResolutionPort, providerTechnicalTestService, pathCheckPort,
technicalTestOrchestrator, correctionExecutionService, batchRunLauncher,
miniRunLauncher, resetPort, manualRenamePort, manualCopyPort,
historicalDocumentContextPort, applicationVersion, promptEditorPort);
historicalDocumentContextPort, applicationVersion, promptEditorPort,
historyOverviewPort, historyDetailsPort, historyResetPort, deleteHistoryPort);
} catch (GuiConfigurationLoadException e) {
LOG.error("GUI startup: configuration could not be loaded, starting without it: {}",
e.getMessage(), e);
@@ -890,7 +914,11 @@ public class BootstrapRunner {
manualCopyPort,
historicalDocumentContextPort,
applicationVersion,
noOpGuiPromptEditorPort());
noOpGuiPromptEditorPort(),
historyOverviewPort,
historyDetailsPort,
historyResetPort,
deleteHistoryPort);
}
}
@@ -1421,6 +1449,133 @@ public class BootstrapRunner {
}
}
/**
* Lädt die gefilterte Dokumentenübersicht für den Historien-Tab.
* <p>
* Verdrahtet {@link SqliteHistoryQueryAdapter} und {@link DefaultHistoryOverviewUseCase}
* frisch pro Aufruf, da keine persistente Verbindung über GUI-Interaktionen hinweg
* gehalten wird.
*
* @param configFilePath Pfad zur geladenen Konfigurationsdatei; darf nicht {@code null} sein
* @param query Abfrageparameter; darf nicht {@code null} sein
* @return Ergebnis mit gefundenen Zeilen und hasMore-Flag; nie {@code null}
*/
DefaultHistoryOverviewUseCase.HistoryOverviewResult loadHistoryOverviewForGui(
Path configFilePath,
de.gecheckt.pdf.umbenenner.application.port.out.history.HistoryQuery query) {
Objects.requireNonNull(configFilePath, "configFilePath must not be null");
Objects.requireNonNull(query, "query must not be null");
try {
migrateConfigurationIfNeeded(configFilePath);
StartConfiguration config = loadAndValidateConfiguration(configFilePath);
initializeSchema(config);
String jdbcUrl = buildJdbcUrl(config);
HistoryQueryPort historyQueryPort = new SqliteHistoryQueryAdapter(jdbcUrl);
DefaultHistoryOverviewUseCase useCase = new DefaultHistoryOverviewUseCase(historyQueryPort);
return useCase.loadOverview(query);
} catch (Exception e) {
LOG.error("Historienübersicht konnte nicht geladen werden: {}", e.getMessage(), e);
throw new DocumentPersistenceException(
"Historienübersicht konnte nicht geladen werden: " + e.getMessage(), e);
}
}
/**
* Lädt Stammsatz und alle Verarbeitungsversuche für den angegebenen Fingerprint.
* <p>
* Verdrahtet {@link SqliteHistoryQueryAdapter} und {@link DefaultHistoryDetailsUseCase}
* frisch pro Aufruf.
*
* @param configFilePath Pfad zur geladenen Konfigurationsdatei; darf nicht {@code null} sein
* @param fingerprint der Dokumentbezeichner; darf nicht {@code null} sein
* @return Optional mit den Detaildaten, oder leer wenn kein Eintrag gefunden wurde
*/
Optional<DefaultHistoryDetailsUseCase.HistoryDetailsResult> loadHistoryDetailsForGui(
Path configFilePath,
DocumentFingerprint fingerprint) {
Objects.requireNonNull(configFilePath, "configFilePath must not be null");
Objects.requireNonNull(fingerprint, "fingerprint must not be null");
try {
migrateConfigurationIfNeeded(configFilePath);
StartConfiguration config = loadAndValidateConfiguration(configFilePath);
initializeSchema(config);
String jdbcUrl = buildJdbcUrl(config);
HistoryQueryPort historyQueryPort = new SqliteHistoryQueryAdapter(jdbcUrl);
DefaultHistoryDetailsUseCase useCase = new DefaultHistoryDetailsUseCase(historyQueryPort);
return useCase.loadDetails(fingerprint);
} catch (Exception e) {
LOG.error("Historiendetails für {} konnten nicht geladen werden: {}",
fingerprint.sha256Hex(), e.getMessage(), e);
throw new DocumentPersistenceException(
"Historiendetails konnten nicht geladen werden: " + e.getMessage(), e);
}
}
/**
* Führt den feldgenauen Status-Reset für das angegebene Dokument durch.
* <p>
* Setzt {@code overall_status}, {@code content_error_count}, {@code transient_error_count}
* und {@code last_failure_instant} zurück. Die Versuchshistorie bleibt erhalten.
*
* @param configFilePath Pfad zur geladenen Konfigurationsdatei; darf nicht {@code null} sein
* @param fingerprint der Dokumentbezeichner; darf nicht {@code null} sein
*/
void resetHistoryDocumentStatusForGui(
Path configFilePath,
DocumentFingerprint fingerprint) {
Objects.requireNonNull(configFilePath, "configFilePath must not be null");
Objects.requireNonNull(fingerprint, "fingerprint must not be null");
LOG.info("Historien-Status-Reset für Fingerprint: {}", fingerprint.sha256Hex());
try {
migrateConfigurationIfNeeded(configFilePath);
StartConfiguration config = loadAndValidateConfiguration(configFilePath);
initializeSchema(config);
String jdbcUrl = buildJdbcUrl(config);
UnitOfWorkPort unitOfWorkPort = new SqliteUnitOfWorkAdapter(jdbcUrl);
DefaultHistoryResetDocumentStatusUseCase useCase =
new DefaultHistoryResetDocumentStatusUseCase(unitOfWorkPort);
useCase.resetStatus(fingerprint);
LOG.info("Historien-Status-Reset abgeschlossen für Fingerprint: {}", fingerprint.sha256Hex());
} catch (Exception e) {
LOG.error("Historien-Status-Reset fehlgeschlagen für {}: {}",
fingerprint.sha256Hex(), e.getMessage(), e);
throw new DocumentPersistenceException(
"Status-Reset fehlgeschlagen: " + e.getMessage(), e);
}
}
/**
* Löscht den Stammsatz und alle Verarbeitungsversuche für das angegebene Dokument.
* <p>
* Die Löschung ist destruktiv und nicht rückgängig zu machen.
*
* @param configFilePath Pfad zur geladenen Konfigurationsdatei; darf nicht {@code null} sein
* @param fingerprint der Dokumentbezeichner; darf nicht {@code null} sein
*/
void deleteDocumentHistoryForGui(
Path configFilePath,
DocumentFingerprint fingerprint) {
Objects.requireNonNull(configFilePath, "configFilePath must not be null");
Objects.requireNonNull(fingerprint, "fingerprint must not be null");
LOG.info("Historien-Löschen für Fingerprint: {}", fingerprint.sha256Hex());
try {
migrateConfigurationIfNeeded(configFilePath);
StartConfiguration config = loadAndValidateConfiguration(configFilePath);
initializeSchema(config);
String jdbcUrl = buildJdbcUrl(config);
UnitOfWorkPort unitOfWorkPort = new SqliteUnitOfWorkAdapter(jdbcUrl);
DefaultDeleteDocumentHistoryUseCase useCase =
new DefaultDeleteDocumentHistoryUseCase(unitOfWorkPort);
useCase.deleteHistory(fingerprint);
LOG.info("Historien-Löschen abgeschlossen für Fingerprint: {}", fingerprint.sha256Hex());
} catch (Exception e) {
LOG.error("Historien-Löschen fehlgeschlagen für {}: {}",
fingerprint.sha256Hex(), e.getMessage(), e);
throw new DocumentPersistenceException(
"Löschen fehlgeschlagen: " + e.getMessage(), e);
}
}
/**
* Builds a {@link ResetDocumentStatusResult} where every requested fingerprint is
* recorded as a failure with the given error message.