diff --git a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/AiFailureMessageTranslator.java b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/AiFailureMessageTranslator.java index 37c0964..5b8b2cc 100644 --- a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/AiFailureMessageTranslator.java +++ b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/AiFailureMessageTranslator.java @@ -83,7 +83,7 @@ public final class AiFailureMessageTranslator { || lower.contains("empty_response") || lower.contains("missing_title") || lower.contains("blank_title") || lower.contains("missing_reasoning")) { - return "KI-Antwort war nicht im erwarteten Format (kein gültiges JSON). " + return "KI-Antwort war nicht im erwarteten Format (kein gültiges JSON oder ein Pflichtfeld fehlt). " + "Bitte erneut verarbeiten; hält der Fehler an, den Prompt anpassen."; } diff --git a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/history/GuiHistoryTab.java b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/history/GuiHistoryTab.java index 8a4d891..dd707f1 100644 --- a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/history/GuiHistoryTab.java +++ b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/history/GuiHistoryTab.java @@ -146,6 +146,13 @@ public final class GuiHistoryTab { private final TextArea reasoningArea = new TextArea(); private final TextArea rawResponseArea = new TextArea(); + /** + * SHA-256 des zuletzt angeforderten Detail-Dokuments. Dient dazu, verspätet + * eintreffende Detail-Ergebnisse einer inzwischen abgewählten oder gewechselten + * Selektion zu verwerfen. Zugriff ausschließlich auf dem JavaFX Application Thread. + */ + private String pendingDetailFingerprintHex; + private final Button resetButton = new Button("Status zurücksetzen"); private final Button deleteButton = new Button("Eintrag löschen"); @@ -715,7 +722,10 @@ public final class GuiHistoryTab { } private void loadDetails(DocumentFingerprint fingerprint) { + pendingDetailFingerprintHex = fingerprint.sha256Hex(); reasoningArea.setText(LOADING_TEXT); + rawResponseArea.setText(""); + rawResponseArea.setPromptText(NO_RAW_RESPONSE_TEXT); attemptsItems.clear(); clearDetailFields(); @@ -729,6 +739,9 @@ public final class GuiHistoryTab { try { Optional result = detailsPort.loadDetails(configPath, fingerprint); Platform.runLater(() -> { + if (isSupersededDetail(fingerprint)) { + return; + } if (result.isEmpty()) { clearDetailPane(); statusBarLabel.setText("Eintrag nicht mehr vorhanden."); @@ -740,6 +753,10 @@ public final class GuiHistoryTab { LOG.error("Fehler beim Laden der Dokumentdetails für {}: {}", fingerprint.sha256Hex(), ex.getMessage(), ex); Platform.runLater(() -> { + if (isSupersededDetail(fingerprint)) { + return; + } + clearDetailPane(); reasoningArea.setText("Fehler beim Laden der Details: " + ex.getMessage()); statusBarLabel.setText("Fehler beim Laden der Details."); }); @@ -957,6 +974,7 @@ public final class GuiHistoryTab { if (!failureRelevant || attempts.isEmpty()) { failureArea.setText(""); failureArea.setPromptText("Keine Fehlerdetails für diesen Status."); + failureArea.setTooltip(new Tooltip(GuiTooltipTexts.VERLAUF_FAILURE_AREA)); return; } @@ -1010,7 +1028,19 @@ public final class GuiHistoryTab { } } + /** + * Prüft, ob ein asynchron geladenes Detail-Ergebnis inzwischen überholt ist, weil + * mittlerweile ein anderes (oder kein) Dokument ausgewählt wurde. + * + * @param fingerprint der Fingerprint, für den das Ergebnis geladen wurde + * @return {@code true}, wenn das Ergebnis verworfen werden soll + */ + private boolean isSupersededDetail(DocumentFingerprint fingerprint) { + return !fingerprint.sha256Hex().equals(pendingDetailFingerprintHex); + } + private void clearDetailPane() { + pendingDetailFingerprintHex = null; clearDetailFields(); attemptsItems.clear(); failureArea.setText(""); diff --git a/pdf-umbenenner-bootstrap/src/main/java/de/gecheckt/pdf/umbenenner/bootstrap/startup/RuntimeLogDirectoryReconfigurer.java b/pdf-umbenenner-bootstrap/src/main/java/de/gecheckt/pdf/umbenenner/bootstrap/startup/RuntimeLogDirectoryReconfigurer.java index 41aa88b..22bd29d 100644 --- a/pdf-umbenenner-bootstrap/src/main/java/de/gecheckt/pdf/umbenenner/bootstrap/startup/RuntimeLogDirectoryReconfigurer.java +++ b/pdf-umbenenner-bootstrap/src/main/java/de/gecheckt/pdf/umbenenner/bootstrap/startup/RuntimeLogDirectoryReconfigurer.java @@ -92,12 +92,17 @@ public class RuntimeLogDirectoryReconfigurer { return LogDirectoryChange.unchanged(); } + String previous = System.getProperty(SYSTEM_PROPERTY_KEY); System.setProperty(SYSTEM_PROPERTY_KEY, target); try { reconfigure(); } catch (RuntimeException e) { - LOG.warn("Log4j2 konnte nicht auf Log-Verzeichnis '{}' umgestellt werden: {}", - target, e.getMessage()); + // Property auf den vorherigen Wert zurücksetzen: Bleibt sie auf dem neuen (nicht + // aktivierten) Ziel stehen, würde die Idempotenz-Prüfung oben jeden weiteren + // Umstellungsversuch fälschlich als „bereits aktiv" überspringen. + restoreProperty(previous); + LOG.warn("Log4j2 konnte nicht auf Log-Verzeichnis '{}' umgestellt werden: {} – " + + "bisheriges Log-Verzeichnis bleibt aktiv.", target, e.getMessage()); return LogDirectoryChange.unchanged(); } @@ -105,6 +110,14 @@ public class RuntimeLogDirectoryReconfigurer { return LogDirectoryChange.changedTo(target); } + private static void restoreProperty(String previous) { + if (previous == null) { + System.clearProperty(SYSTEM_PROPERTY_KEY); + } else { + System.setProperty(SYSTEM_PROPERTY_KEY, previous); + } + } + /** * Konfiguriert den laufenden Log4j2-{@link LoggerContext} neu, sodass * {@code ${sys:log.directory}} erneut aufgelöst wird. diff --git a/pdf-umbenenner-bootstrap/src/test/java/de/gecheckt/pdf/umbenenner/bootstrap/startup/RuntimeLogDirectoryReconfigurerTest.java b/pdf-umbenenner-bootstrap/src/test/java/de/gecheckt/pdf/umbenenner/bootstrap/startup/RuntimeLogDirectoryReconfigurerTest.java index d1885d4..268a34d 100644 --- a/pdf-umbenenner-bootstrap/src/test/java/de/gecheckt/pdf/umbenenner/bootstrap/startup/RuntimeLogDirectoryReconfigurerTest.java +++ b/pdf-umbenenner-bootstrap/src/test/java/de/gecheckt/pdf/umbenenner/bootstrap/startup/RuntimeLogDirectoryReconfigurerTest.java @@ -96,6 +96,24 @@ class RuntimeLogDirectoryReconfigurerTest { assertEquals(0, reconf.reconfigureCalls); } + @Test + void apply_reconfigureFails_restoresPreviousProperty() { + System.setProperty(KEY, "C:/vorher/logs"); + RuntimeLogDirectoryReconfigurer failing = new RuntimeLogDirectoryReconfigurer() { + @Override + protected void reconfigure() { + throw new IllegalStateException("reconfigure kaputt"); + } + }; + Path target = tempDir.resolve("neu"); + + LogDirectoryChange change = failing.apply(target); + + assertFalse(change.changed()); + assertEquals("C:/vorher/logs", System.getProperty(KEY), + "Nach fehlgeschlagener Neukonfiguration muss der vorherige Property-Wert wiederhergestellt sein"); + } + @Test void apply_nonCreatableDirectory_leavesPropertyUnchanged() throws IOException { RecordingReconfigurer reconf = new RecordingReconfigurer();