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("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.";
}
@@ -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<HistoryDetailsResult> 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("");
@@ -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.
@@ -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();