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 9fc3191..580eb6b 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 @@ -127,6 +127,7 @@ public final class GuiHistoryTab { private final TableView attemptsTable = new TableView<>(); private final ObservableList 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 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); } diff --git a/pdf-umbenenner-adapter-out/src/main/java/de/gecheckt/pdf/umbenenner/adapter/out/sqlite/SqliteProcessingAttemptRepositoryAdapter.java b/pdf-umbenenner-adapter-out/src/main/java/de/gecheckt/pdf/umbenenner/adapter/out/sqlite/SqliteProcessingAttemptRepositoryAdapter.java index b53d86f..74e8c71 100644 --- a/pdf-umbenenner-adapter-out/src/main/java/de/gecheckt/pdf/umbenenner/adapter/out/sqlite/SqliteProcessingAttemptRepositoryAdapter.java +++ b/pdf-umbenenner-adapter-out/src/main/java/de/gecheckt/pdf/umbenenner/adapter/out/sqlite/SqliteProcessingAttemptRepositoryAdapter.java @@ -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 // -------------------------------------------------------------------------