fix: Codex-Review-Befunde zu Verlauf-Detailanzeige und Log-Umstellung

- RuntimeLogDirectoryReconfigurer: Bei fehlgeschlagenem Log4j2-Reconfigure wird die
  System-Property log.directory auf den vorherigen Wert zurückgesetzt, damit ein
  späterer Versuch nicht faelschlich als "bereits aktiv" uebersprungen wird. Test ergaenzt.
- GuiHistoryTab: Beim Detailwechsel wird die KI-Rohantwort zurückgesetzt; verspätet
  eintreffende Detail-Ergebnisse einer inzwischen gewechselten/abgewählten Selektion
  werden über einen Fingerprint-Abgleich verworfen (kein "A unter B", keine dauerhaft
  sichtbare fremde Rohantwort bei Ladefehler).
- GuiHistoryTab: Der technische Fehler-Tooltip wird bei nicht-fehlerhaften Datensätzen
  auf den Standardtext zurückgesetzt (keine fremde Fehlermeldung mehr sichtbar).
- AiFailureMessageTranslator: praezisere Meldung ("kein gueltiges JSON oder ein
  Pflichtfeld fehlt").

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-01 18:53:46 +02:00
parent a3ed5329af
commit c7a40298df
4 changed files with 64 additions and 3 deletions
@@ -83,7 +83,7 @@ public final class AiFailureMessageTranslator {
|| lower.contains("empty_response") || lower.contains("empty_response")
|| lower.contains("missing_title") || lower.contains("blank_title") || lower.contains("missing_title") || lower.contains("blank_title")
|| lower.contains("missing_reasoning")) { || 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."; + "Bitte erneut verarbeiten; hält der Fehler an, den Prompt anpassen.";
} }
@@ -146,6 +146,13 @@ public final class GuiHistoryTab {
private final TextArea reasoningArea = new TextArea(); private final TextArea reasoningArea = new TextArea();
private final TextArea rawResponseArea = 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 resetButton = new Button("Status zurücksetzen");
private final Button deleteButton = new Button("Eintrag löschen"); private final Button deleteButton = new Button("Eintrag löschen");
@@ -715,7 +722,10 @@ public final class GuiHistoryTab {
} }
private void loadDetails(DocumentFingerprint fingerprint) { private void loadDetails(DocumentFingerprint fingerprint) {
pendingDetailFingerprintHex = fingerprint.sha256Hex();
reasoningArea.setText(LOADING_TEXT); reasoningArea.setText(LOADING_TEXT);
rawResponseArea.setText("");
rawResponseArea.setPromptText(NO_RAW_RESPONSE_TEXT);
attemptsItems.clear(); attemptsItems.clear();
clearDetailFields(); clearDetailFields();
@@ -729,6 +739,9 @@ public final class GuiHistoryTab {
try { try {
Optional<HistoryDetailsResult> result = detailsPort.loadDetails(configPath, fingerprint); Optional<HistoryDetailsResult> result = detailsPort.loadDetails(configPath, fingerprint);
Platform.runLater(() -> { Platform.runLater(() -> {
if (isSupersededDetail(fingerprint)) {
return;
}
if (result.isEmpty()) { if (result.isEmpty()) {
clearDetailPane(); clearDetailPane();
statusBarLabel.setText("Eintrag nicht mehr vorhanden."); statusBarLabel.setText("Eintrag nicht mehr vorhanden.");
@@ -740,6 +753,10 @@ public final class GuiHistoryTab {
LOG.error("Fehler beim Laden der Dokumentdetails für {}: {}", LOG.error("Fehler beim Laden der Dokumentdetails für {}: {}",
fingerprint.sha256Hex(), ex.getMessage(), ex); fingerprint.sha256Hex(), ex.getMessage(), ex);
Platform.runLater(() -> { Platform.runLater(() -> {
if (isSupersededDetail(fingerprint)) {
return;
}
clearDetailPane();
reasoningArea.setText("Fehler beim Laden der Details: " + ex.getMessage()); reasoningArea.setText("Fehler beim Laden der Details: " + ex.getMessage());
statusBarLabel.setText("Fehler beim Laden der Details."); statusBarLabel.setText("Fehler beim Laden der Details.");
}); });
@@ -957,6 +974,7 @@ public final class GuiHistoryTab {
if (!failureRelevant || attempts.isEmpty()) { if (!failureRelevant || attempts.isEmpty()) {
failureArea.setText(""); failureArea.setText("");
failureArea.setPromptText("Keine Fehlerdetails für diesen Status."); failureArea.setPromptText("Keine Fehlerdetails für diesen Status.");
failureArea.setTooltip(new Tooltip(GuiTooltipTexts.VERLAUF_FAILURE_AREA));
return; 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() { private void clearDetailPane() {
pendingDetailFingerprintHex = null;
clearDetailFields(); clearDetailFields();
attemptsItems.clear(); attemptsItems.clear();
failureArea.setText(""); failureArea.setText("");
@@ -92,12 +92,17 @@ public class RuntimeLogDirectoryReconfigurer {
return LogDirectoryChange.unchanged(); return LogDirectoryChange.unchanged();
} }
String previous = System.getProperty(SYSTEM_PROPERTY_KEY);
System.setProperty(SYSTEM_PROPERTY_KEY, target); System.setProperty(SYSTEM_PROPERTY_KEY, target);
try { try {
reconfigure(); reconfigure();
} catch (RuntimeException e) { } catch (RuntimeException e) {
LOG.warn("Log4j2 konnte nicht auf Log-Verzeichnis '{}' umgestellt werden: {}", // Property auf den vorherigen Wert zurücksetzen: Bleibt sie auf dem neuen (nicht
target, e.getMessage()); // 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(); return LogDirectoryChange.unchanged();
} }
@@ -105,6 +110,14 @@ public class RuntimeLogDirectoryReconfigurer {
return LogDirectoryChange.changedTo(target); 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 * Konfiguriert den laufenden Log4j2-{@link LoggerContext} neu, sodass
* {@code ${sys:log.directory}} erneut aufgelöst wird. * {@code ${sys:log.directory}} erneut aufgelöst wird.
@@ -96,6 +96,24 @@ class RuntimeLogDirectoryReconfigurerTest {
assertEquals(0, reconf.reconfigureCalls); 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 @Test
void apply_nonCreatableDirectory_leavesPropertyUnchanged() throws IOException { void apply_nonCreatableDirectory_leavesPropertyUnchanged() throws IOException {
RecordingReconfigurer reconf = new RecordingReconfigurer(); RecordingReconfigurer reconf = new RecordingReconfigurer();