V2.9: Integrierte PDF-Vorschau und editierbarer Dateiname im Verarbeitungslauf
Neu im Tab "Verarbeitungslauf": - Integrierte PDF-Vorschau der Quelldatei mit Lazy Rendering (Seite 1 sofort, weitere Seiten on-demand), Cache pro Selektion, "latest preview request wins" - Editierbarer KI-Dateinamenvorschlag mit Live-Validierung, Dirty-State-Dialog bei Zeilen-/Tabwechsel, Schließen und Laufstart, atomare FS+DB-Transaktion inkl. Rollback und Fingerprint-basierter Konfliktauflösung Architektur: - Neuer Application-Use-Case ManualFileRenameUseCase und Outbound-Port TargetFileRenamePort mit Filesystem-Adapter - Neuer GuiManualFileRenamePort, verdrahtet im Bootstrap - GuiBatchRunResultRow um correctedFileName erweitert - GuiBatchRunTab auf SplitPane-Layout (60/40) umgebaut, Detail-Panel mit KI-Begründung, FileNameEditorPane und PdfPreviewPane - Spike-Code (PdfViewerSpike) entfernt, produktive Implementierung ersetzt Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+112
-4
@@ -24,6 +24,7 @@ import de.gecheckt.pdf.umbenenner.adapter.in.gui.GuiConfigurationLoadException;
|
||||
import de.gecheckt.pdf.umbenenner.adapter.in.gui.GuiStartupContext;
|
||||
import de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun.GuiBatchRunLaunchOutcome;
|
||||
import de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun.GuiBatchRunLauncher;
|
||||
import de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun.GuiManualFileRenamePort;
|
||||
import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiConfigurationEditorState;
|
||||
import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiConfigurationEditorStateFactory;
|
||||
import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiConfigurationFileSnapshot;
|
||||
@@ -46,6 +47,7 @@ import de.gecheckt.pdf.umbenenner.adapter.out.sqlite.SqliteProcessingAttemptRepo
|
||||
import de.gecheckt.pdf.umbenenner.adapter.out.sqlite.SqliteSchemaInitializationAdapter;
|
||||
import de.gecheckt.pdf.umbenenner.adapter.out.sqlite.SqliteUnitOfWorkAdapter;
|
||||
import de.gecheckt.pdf.umbenenner.adapter.out.targetcopy.FilesystemTargetFileCopyAdapter;
|
||||
import de.gecheckt.pdf.umbenenner.adapter.out.targetfolder.FilesystemTargetFileRenameAdapter;
|
||||
import de.gecheckt.pdf.umbenenner.adapter.out.targetfolder.FilesystemTargetFolderAdapter;
|
||||
import de.gecheckt.pdf.umbenenner.application.config.RuntimeConfiguration;
|
||||
import de.gecheckt.pdf.umbenenner.application.config.provider.AiProviderFamily;
|
||||
@@ -53,6 +55,9 @@ import de.gecheckt.pdf.umbenenner.application.config.provider.ProviderConfigurat
|
||||
import de.gecheckt.pdf.umbenenner.application.config.startup.StartConfiguration;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.in.BatchRunOutcome;
|
||||
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.ResetDocumentStatusResult;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.AiContentSensitivity;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.AiInvocationPort;
|
||||
@@ -67,6 +72,7 @@ import de.gecheckt.pdf.umbenenner.application.port.out.ProcessingLogger;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.PromptPort;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.RunLockPort;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.TargetFileCopyPort;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.TargetFileRenamePort;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.TargetFolderPort;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.UnitOfWorkPort;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.AiModelCatalogPort;
|
||||
@@ -74,6 +80,7 @@ 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.application.usecase.DefaultBatchRunProcessingUseCase;
|
||||
import de.gecheckt.pdf.umbenenner.application.usecase.DefaultManualFileRenameUseCase;
|
||||
import de.gecheckt.pdf.umbenenner.application.usecase.DefaultResetDocumentStatusUseCase;
|
||||
import de.gecheckt.pdf.umbenenner.application.validation.editor.EditorConfigurationValidator;
|
||||
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ProviderTechnicalTestService;
|
||||
@@ -675,6 +682,7 @@ public class BootstrapRunner {
|
||||
this::launchGuiMiniBatchRun;
|
||||
de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun.GuiResetDocumentStatusPort resetPort =
|
||||
this::resetDocumentStatusForGui;
|
||||
GuiManualFileRenamePort manualRenamePort = this::performGuiManualFileRename;
|
||||
|
||||
if (configPathOverride.isEmpty()) {
|
||||
return new GuiStartupContext(
|
||||
@@ -690,7 +698,8 @@ public class BootstrapRunner {
|
||||
correctionExecutionService,
|
||||
batchRunLauncher,
|
||||
miniRunLauncher,
|
||||
resetPort);
|
||||
resetPort,
|
||||
manualRenamePort);
|
||||
}
|
||||
|
||||
Path configPath = Paths.get(configPathOverride.get());
|
||||
@@ -711,7 +720,8 @@ public class BootstrapRunner {
|
||||
correctionExecutionService,
|
||||
batchRunLauncher,
|
||||
miniRunLauncher,
|
||||
resetPort);
|
||||
resetPort,
|
||||
manualRenamePort);
|
||||
}
|
||||
|
||||
LOG.info("GUI startup: configuration file confirmed at: {}", configPath.toAbsolutePath());
|
||||
@@ -720,7 +730,7 @@ public class BootstrapRunner {
|
||||
return new GuiStartupContext(loadedState, Optional.empty(), loader, writer,
|
||||
modelCatalogPort, apiKeyResolutionPort, providerTechnicalTestService, pathCheckPort,
|
||||
technicalTestOrchestrator, correctionExecutionService, batchRunLauncher,
|
||||
miniRunLauncher, resetPort);
|
||||
miniRunLauncher, resetPort, manualRenamePort);
|
||||
} catch (GuiConfigurationLoadException e) {
|
||||
LOG.error("GUI startup: configuration could not be loaded, starting without it: {}",
|
||||
e.getMessage(), e);
|
||||
@@ -737,7 +747,8 @@ public class BootstrapRunner {
|
||||
correctionExecutionService,
|
||||
batchRunLauncher,
|
||||
miniRunLauncher,
|
||||
resetPort);
|
||||
resetPort,
|
||||
manualRenamePort);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -954,6 +965,103 @@ public class BootstrapRunner {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt einen vollständig verdrahteten {@link ManualFileRenameUseCase} für den
|
||||
* gegebenen Startkonfigurations-Stand.
|
||||
* <p>
|
||||
* Teilt die Wiring-Konventionen mit dem Batch-Pfad: SQLite-URL-Aufbau, Adapter-Instanzen
|
||||
* und Logger-Konfiguration werden nach dem gleichen Muster erzeugt.
|
||||
*
|
||||
* @param startConfig die validierte Startkonfiguration; darf nicht null sein
|
||||
* @return ein einsatzbereiter Use-Case; nie null
|
||||
*/
|
||||
private ManualFileRenameUseCase buildProductionManualFileRenameUseCase(
|
||||
StartConfiguration startConfig) {
|
||||
String jdbcUrl = buildJdbcUrl(startConfig);
|
||||
DocumentRecordRepository documentRecordRepository =
|
||||
new SqliteDocumentRecordRepositoryAdapter(jdbcUrl);
|
||||
UnitOfWorkPort unitOfWorkPort = new SqliteUnitOfWorkAdapter(jdbcUrl);
|
||||
TargetFolderPort targetFolderPort =
|
||||
new FilesystemTargetFolderAdapter(startConfig.targetFolder());
|
||||
TargetFileRenamePort targetFileRenamePort =
|
||||
new FilesystemTargetFileRenameAdapter(startConfig.targetFolder());
|
||||
ClockPort clockPort = new SystemClockAdapter();
|
||||
AiContentSensitivity aiContentSensitivity =
|
||||
resolveAiContentSensitivity(startConfig.logAiSensitive());
|
||||
ProcessingLogger processingLogger = new Log4jProcessingLogger(
|
||||
DefaultManualFileRenameUseCase.class, aiContentSensitivity);
|
||||
return new DefaultManualFileRenameUseCase(
|
||||
documentRecordRepository,
|
||||
targetFolderPort,
|
||||
targetFileRenamePort,
|
||||
unitOfWorkPort,
|
||||
clockPort,
|
||||
processingLogger);
|
||||
}
|
||||
|
||||
/**
|
||||
* Führt eine manuelle Umbenennung einer Zieldatei durch, ausgelöst von der GUI.
|
||||
* <p>
|
||||
* Lädt und validiert die Konfiguration aus {@code configFilePath}, baut den
|
||||
* Use-Case auf und delegiert die Umbenennung. Alle Fehler beim Laden oder
|
||||
* Validieren der Konfiguration werden als strukturiertes {@link ManualFileRenameResult}
|
||||
* zurückgegeben.
|
||||
*
|
||||
* @param configFilePath Pfad zur {@code .properties}-Datei; muss existieren
|
||||
* @param request die Umbenennungsanfrage; darf nicht null sein
|
||||
* @return das Ergebnis der Umbenennung; nie null
|
||||
*/
|
||||
ManualFileRenameResult performGuiManualFileRename(
|
||||
Path configFilePath,
|
||||
ManualFileRenameRequest request) {
|
||||
Objects.requireNonNull(configFilePath, "configFilePath must not be null");
|
||||
Objects.requireNonNull(request, "request must not be null");
|
||||
LOG.info("GUI-Umbenennung: Anfrage für Fingerprint={}, Zielname={}.",
|
||||
request.fingerprint().sha256Hex(), request.desiredBaseFileName());
|
||||
|
||||
if (!Files.exists(configFilePath)) {
|
||||
String msg = "Konfigurationsdatei nicht gefunden: " + configFilePath;
|
||||
LOG.error("GUI-Umbenennung: {}", msg);
|
||||
return new de.gecheckt.pdf.umbenenner.application.port.in
|
||||
.ManualFileRenameFileSystemFailure(msg);
|
||||
}
|
||||
|
||||
try {
|
||||
migrateConfigurationIfNeeded(configFilePath);
|
||||
StartConfiguration config = loadAndValidateConfiguration(configFilePath);
|
||||
initializeSchema(config);
|
||||
ManualFileRenameUseCase useCase = buildProductionManualFileRenameUseCase(config);
|
||||
ManualFileRenameResult result = useCase.rename(request);
|
||||
LOG.info("GUI-Umbenennung abgeschlossen: Ergebnis={}.", result.getClass().getSimpleName());
|
||||
return result;
|
||||
} catch (ConfigurationLoadingException e) {
|
||||
LOG.error("GUI-Umbenennung: Konfiguration konnte nicht geladen werden: {}",
|
||||
e.getMessage(), e);
|
||||
return new de.gecheckt.pdf.umbenenner.application.port.in
|
||||
.ManualFileRenamePersistenceFailure(
|
||||
"Konfiguration konnte nicht geladen werden: " + e.getMessage());
|
||||
} catch (InvalidStartConfigurationException e) {
|
||||
LOG.error("GUI-Umbenennung: Konfiguration ist nicht lauffähig: {}", e.getMessage());
|
||||
return new de.gecheckt.pdf.umbenenner.application.port.in
|
||||
.ManualFileRenamePersistenceFailure(
|
||||
"Die Konfiguration ist nicht lauffähig: " + e.getMessage());
|
||||
} catch (DocumentPersistenceException e) {
|
||||
LOG.error("GUI-Umbenennung: SQLite-Initialisierung fehlgeschlagen: {}",
|
||||
e.getMessage(), e);
|
||||
return new de.gecheckt.pdf.umbenenner.application.port.in
|
||||
.ManualFileRenamePersistenceFailure(
|
||||
"SQLite-Datenbank konnte nicht vorbereitet werden: " + e.getMessage());
|
||||
} catch (RuntimeException e) {
|
||||
LOG.error("GUI-Umbenennung: Unerwarteter Fehler: {}", e.getMessage(), e);
|
||||
return new de.gecheckt.pdf.umbenenner.application.port.in
|
||||
.ManualFileRenameFileSystemFailure(
|
||||
"Unerwarteter Fehler: "
|
||||
+ (e.getMessage() == null
|
||||
? e.getClass().getSimpleName()
|
||||
: e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a {@link ResetDocumentStatusResult} where every requested fingerprint is
|
||||
* recorded as a failure with the given error message.
|
||||
|
||||
Reference in New Issue
Block a user