#88: Fehlerursache bei FAILED_FINAL im Verlauf-Tab anzeigen (Fall A)

Schema-Analyse ergab Fall A: failure_message ist bereits in V1 vorhanden
und wird persistiert. Keine Flyway-Migration notwendig.

- GuiHistoryTab: TextArea 'Fehlerursache' ergaenzt; zeigt failure_message
  des letzten Fehler-Attempts bei FAILED_FINAL, FAILED_RETRYABLE,
  SKIPPED_FINAL_FAILURE; promptText-Platzhalter bei NULL/leer
- SqliteProcessingAttemptRepositoryAdapter: 1000-Zeichen-Limit fuer
  failure_message vor Persistierung erzwungen (mit Kuerzungsmarkierung)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-05 11:46:37 +02:00
parent 38b2d8c3b2
commit cec3b4fb84
2 changed files with 68 additions and 1 deletions
@@ -127,6 +127,7 @@ public final class GuiHistoryTab {
private final TableView<ProcessingAttempt> attemptsTable = new TableView<>();
private final ObservableList<ProcessingAttempt> attemptsItems = FXCollections.observableArrayList();
private final TextArea failureArea = new TextArea();
private final TextArea reasoningArea = new TextArea();
private final Button resetButton = new Button("Status zurücksetzen");
@@ -339,6 +340,14 @@ public final class GuiHistoryTab {
Label attemptsTitle = new Label("Verarbeitungsversuche");
attemptsTitle.setStyle("-fx-font-weight: bold;");
// Fehlerursache (aus letztem Fehler-Versuch)
failureArea.setEditable(false);
failureArea.setWrapText(true);
failureArea.setPrefRowCount(3);
failureArea.setPromptText("Keine Fehlerdetails gespeichert.");
Label failureTitle = new Label("Fehlerursache (letzter Fehler-Versuch)");
failureTitle.setStyle("-fx-font-weight: bold;");
// KI-Begründung
reasoningArea.setEditable(false);
reasoningArea.setWrapText(true);
@@ -350,6 +359,7 @@ public final class GuiHistoryTab {
VBox rightPane = new VBox(8,
detailTitle, detailGrid,
attemptsTitle, attemptsTable,
failureTitle, failureArea,
reasoningTitle, reasoningArea);
rightPane.setPadding(new Insets(4, 8, 4, 4));
VBox.setVgrow(attemptsTable, Priority.ALWAYS);
@@ -658,6 +668,9 @@ public final class GuiHistoryTab {
attemptsItems.setAll(result.attempts());
// Fehlerursache aus letztem Fehler-Versuch anzeigen
showLastFailureMessage(result.attempts(), record.overallStatus());
// Neuesten Versuch selektieren und Begründung anzeigen
if (!result.attempts().isEmpty()) {
ProcessingAttempt last = result.attempts().get(result.attempts().size() - 1);
@@ -676,6 +689,36 @@ public final class GuiHistoryTab {
});
}
/**
* Zeigt die Fehlerursache des letzten Fehlschlags im Fehlerursache-Bereich an.
* Relevant bei Status FAILED_FINAL, FAILED_RETRYABLE und SKIPPED_FINAL_FAILURE.
* Bei fehlendem Eintrag oder leerem Feld wird ein Platzhalter-Text gesetzt.
*/
private void showLastFailureMessage(List<ProcessingAttempt> attempts, ProcessingStatus overallStatus) {
boolean failureRelevant = overallStatus == ProcessingStatus.FAILED_FINAL
|| overallStatus == ProcessingStatus.FAILED_RETRYABLE
|| overallStatus == ProcessingStatus.SKIPPED_FINAL_FAILURE;
if (!failureRelevant || attempts.isEmpty()) {
failureArea.setText("");
failureArea.setPromptText("Keine Fehlerdetails für diesen Status.");
return;
}
// Letzten Versuch mit nicht-leerem failure_message suchen (absteigend nach attempt_number)
String failureMessage = null;
for (int i = attempts.size() - 1; i >= 0; i--) {
String msg = attempts.get(i).failureMessage();
if (msg != null && !msg.isBlank()) {
failureMessage = msg;
break;
}
}
failureArea.setText(failureMessage != null ? failureMessage : "");
failureArea.setPromptText("Keine Fehlerdetails gespeichert.");
}
private void showReasoning(ProcessingAttempt attempt) {
String reasoning = attempt.aiReasoning();
reasoningArea.setText(reasoning != null && !reasoning.isBlank()
@@ -685,6 +728,8 @@ public final class GuiHistoryTab {
private void clearDetailPane() {
clearDetailFields();
attemptsItems.clear();
failureArea.setText("");
failureArea.setPromptText("Keine Fehlerdetails gespeichert.");
reasoningArea.setText(DETAIL_PLACEHOLDER);
}
@@ -159,7 +159,8 @@ public class SqliteProcessingAttemptRepositoryAdapter implements ProcessingAttem
statement.setString(5, attempt.endedAt().toString());
statement.setString(6, attempt.status().name());
setNullableString(statement, 7, attempt.failureClass());
setNullableString(statement, 8, attempt.failureMessage());
// 1000-Zeichen-Grenze erzwingen; längere Meldungen werden mit „…" markiert
setNullableString(statement, 8, truncateFailureMessage(attempt.failureMessage()));
statement.setBoolean(9, attempt.retryable());
// AI provider identifier and AI traceability fields
setNullableString(statement, 10, attempt.aiProvider());
@@ -360,6 +361,27 @@ public class SqliteProcessingAttemptRepositoryAdapter implements ProcessingAttem
}
}
// -------------------------------------------------------------------------
// Hilfsmethoden
// -------------------------------------------------------------------------
/**
* Kürzt eine Fehlermeldung auf maximal 1000 Zeichen vor der Persistierung.
* Längere Meldungen werden mit „…" markiert.
*
* @param message die ursprüngliche Fehlermeldung; kann {@code null} sein
* @return die (ggf. gekürzte) Meldung oder {@code null}
*/
private static String truncateFailureMessage(String message) {
if (message == null) {
return null;
}
if (message.length() <= 1000) {
return message;
}
return message.substring(0, 997) + "";
}
// -------------------------------------------------------------------------
// JDBC nullable helpers
// -------------------------------------------------------------------------