GUI-Bugfixes: Defaults beim Start, kopierbare Meldungen mit Zeitstempel, Befundauflistung, Modell-ComboBox links, effektiver API-Key für Modellabruf

- Blank-Startzustand zeigt jetzt die Standardvorlage (wie nach "Neu"), neue Factory createEmptyStartState für Tests
- Meldungsbereich ist per Kontextmenü bzw. Strg+C kopierbar
- Jede Meldung trägt ein führendes [HH:mm:ss]-Präfix
- Validieren- und Tests-Aktionen akkumulieren Meldungen, automatische Validierung ersetzt still ihre Einträge
- Validieren-Meldung listet alle konkreten Befunde einzeln auf
- Modell-ComboBox und manuelles Modellfeld sind linksbündig
- ApiKeyResolutionPort liefert jetzt den effektiven API-Schlüsselwert (Default + Env-Adapter-Override), so dass der Modellliste-Test in den technischen Tests nicht mehr "API-Schlüssel fehlt" meldet, obwohl er gesetzt ist
This commit is contained in:
2026-04-21 16:04:15 +02:00
parent 6babdd226e
commit ada7e203e3
18 changed files with 471 additions and 204 deletions
@@ -48,12 +48,16 @@ import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ComboBox;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Label;
import javafx.scene.control.MenuItem;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.Separator;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.control.TextField;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
@@ -328,7 +332,7 @@ public final class GuiConfigurationEditorWorkspace {
this.configurationFileLoader = effectiveContext.configurationFileLoader();
this.configurationFileWriter = effectiveContext.configurationFileWriter();
this.editorState = effectiveContext.initialState();
this.welcomeGuidanceVisible = editorState.isNewConfiguration();
this.welcomeGuidanceVisible = false;
this.apiKeyResolutionPort = effectiveContext.apiKeyResolutionPort();
this.modelCatalogCoordinator = new GuiModelCatalogCoordinator(
@@ -1587,10 +1591,12 @@ public final class GuiConfigurationEditorWorkspace {
claudeState.model(),
claudeState.timeoutSeconds(),
claudeKeyDescriptor,
claudeState.apiKey().propertyValue(),
openaiState.baseUrl(),
openaiState.model(),
openaiState.timeoutSeconds(),
openaiKeyDescriptor);
openaiKeyDescriptor,
openaiState.apiKey().propertyValue());
}
/**
@@ -1644,11 +1650,10 @@ public final class GuiConfigurationEditorWorkspace {
* writing anything to disk or making any network or filesystem calls. It is therefore
* safe to call on the FX Application Thread at any time.
* <p>
* In addition to updating the findings shown by the automatic validation, this action
* appends a dedicated INFO message to the central message area to confirm to the user
* that the action was explicitly executed and to report the number of findings found.
* The message uses a distinct source tag so that it can be replaced on subsequent
* executions without removing messages from other sources.
* In addition to updating the field-level findings shown by the automatic validation,
* this action appends a dedicated INFO confirmation message plus one message per concrete
* finding to the central message area. These entries accumulate across repeated clicks
* so the user can compare successive runs.
* <p>
* Differences from the automatic background validation:
* <ul>
@@ -1663,24 +1668,35 @@ public final class GuiConfigurationEditorWorkspace {
private void runValidationAction() {
LOG.info("Aktion Validieren ausgeführt.");
// Re-run in-memory validation; this updates pendingMessages, pendingFieldFindings
// and lastValidationResult identically to the automatic background check.
runEditorValidation();
EditorValidationInput input = buildValidationInput();
EditorValidationReport report = editorValidator.validate(input);
// Replace any previous action-confirmation message; preserve all other messages.
pendingMessages.removeIf(m -> m.source().isPresent()
&& "Validierung-Aktion".equals(m.source().get()));
int findingCount = lastValidationResult.fieldFindings().size();
String confirmationText;
if (findingCount == 0) {
confirmationText = "Aktion Validieren wurde ausgeführt. Keine Befunde.";
} else {
confirmationText = "Aktion Validieren wurde ausgeführt. "
+ findingCount + " Befund" + (findingCount == 1 ? "" : "e") + " gefunden.";
// Update field-level findings (drives red labels under problematic inputs).
pendingFieldFindings.clear();
for (EditorValidationFinding finding : report.findings()) {
if (finding.hasFieldKey()) {
pendingFieldFindings.add(new GuiFieldFinding(finding.fieldKey().orElseThrow(),
toGuiSeverity(finding.severity()), finding.message()));
}
}
// Drop silent auto-validation entries so the central message area is not flooded
// by keystroke-level background checks; explicit action entries always accumulate.
pendingMessages.removeIf(m -> m.source().isPresent()
&& "Validierung".equals(m.source().get()));
// Append a timestamped confirmation plus each concrete finding as its own entry.
int findingCount = report.findings().size();
String confirmationText = findingCount == 0
? "Aktion Validieren wurde ausgeführt. Keine Befunde."
: "Aktion Validieren wurde ausgeführt. "
+ findingCount + " Befund" + (findingCount == 1 ? "" : "e") + " gefunden:";
pendingMessages.add(GuiMessageEntry.of(
GuiMessageSeverity.INFO, confirmationText, "Validierung-Aktion"));
for (EditorValidationFinding finding : report.findings()) {
pendingMessages.add(GuiMessageEntry.of(
toGuiSeverity(finding.severity()), finding.message(), "Validierung-Aktion"));
}
lastValidationResult = new GuiEditorValidationResult(
List.copyOf(pendingMessages),
@@ -1727,13 +1743,19 @@ public final class GuiConfigurationEditorWorkspace {
* list.
* <p>
* Each message is rendered as one {@link TextFlow} row. The severity prefix is coloured using
* the CSS colour from {@link GuiMessageSeverity#getPrefixCssColour()}; the remainder of the
* message text is always black. The placeholder text is shown when the list is empty.
* the CSS colour from {@link GuiMessageSeverity#getPrefixCssColour()} and carries the message
* timestamp in {@code [HH:mm:ss]} form; the remainder of the message text is always black.
* The placeholder text is shown when the list is empty.
* <p>
* Each message row exposes a context menu with a "Kopieren" action that copies the full line
* (timestamp, severity label, body text) to the system clipboard. The container also offers
* an "Alles kopieren" menu entry that concatenates all current messages in display order.
* <p>
* Must be called on the JavaFX Application Thread.
*/
void refreshMessagesArea() {
messagesAreaBox.getChildren().clear();
attachAllCopyContextMenu();
if (pendingMessages.isEmpty()) {
Text placeholder = new Text("Keine Meldungen vorhanden.");
placeholder.setStyle("-fx-fill: #888888;");
@@ -1741,17 +1763,100 @@ public final class GuiConfigurationEditorWorkspace {
return;
}
for (GuiMessageEntry entry : pendingMessages) {
Text prefix = new Text(entry.severity().getPrefixLabel() + " ");
String timestampLabel = formatTimestamp(entry.timestamp());
String severityLabel = entry.severity().getPrefixLabel();
Text prefix = new Text(timestampLabel + " " + severityLabel + " ");
prefix.setStyle("-fx-fill: " + entry.severity().getPrefixCssColour() + ";"
+ " -fx-font-weight: bold;");
Text body = new Text(entry.text());
body.setStyle("-fx-fill: black;");
TextFlow row = new TextFlow(prefix, body);
row.setStyle("-fx-padding: 1px 4px;");
String fullLine = timestampLabel + " " + severityLabel + " " + entry.text();
attachRowCopyContextMenu(row, fullLine);
messagesAreaBox.getChildren().add(row);
}
}
/**
* Installs a context menu on the messages container that copies all rendered lines to the
* system clipboard. Each rendered row receives its own context menu; this menu handles
* clicks on empty areas of the container.
*/
private void attachAllCopyContextMenu() {
ContextMenu menu = new ContextMenu();
MenuItem copyAll = new MenuItem("Alles kopieren");
copyAll.setOnAction(e -> copyAllMessagesToClipboard());
menu.getItems().add(copyAll);
messagesAreaBox.setOnContextMenuRequested(e -> {
if (!pendingMessages.isEmpty()) {
menu.show(messagesAreaBox, e.getScreenX(), e.getScreenY());
}
});
}
/**
* Installs a per-row context menu that copies the row's full textual content and also
* offers the container-level "Alles kopieren" action.
*
* @param row the message row node; must not be {@code null}
* @param fullLine the full text of this row to copy when "Kopieren" is selected
*/
private void attachRowCopyContextMenu(TextFlow row, String fullLine) {
ContextMenu menu = new ContextMenu();
MenuItem copyRow = new MenuItem("Kopieren");
copyRow.setOnAction(e -> writeToClipboard(fullLine));
MenuItem copyAll = new MenuItem("Alles kopieren");
copyAll.setOnAction(e -> copyAllMessagesToClipboard());
menu.getItems().addAll(copyRow, copyAll);
row.setOnContextMenuRequested(e ->
menu.show(row, e.getScreenX(), e.getScreenY()));
}
/**
* Copies all current message lines to the system clipboard in display order.
*/
private void copyAllMessagesToClipboard() {
if (pendingMessages.isEmpty()) {
return;
}
StringBuilder sb = new StringBuilder();
for (GuiMessageEntry entry : pendingMessages) {
sb.append(formatTimestamp(entry.timestamp()))
.append(" ")
.append(entry.severity().getPrefixLabel())
.append(" ")
.append(entry.text())
.append(System.lineSeparator());
}
writeToClipboard(sb.toString().trim());
}
/**
* Writes plain text to the system clipboard.
*
* @param text the text to copy; {@code null} is treated as empty
*/
private void writeToClipboard(String text) {
ClipboardContent content = new ClipboardContent();
content.putString(text == null ? "" : text);
Clipboard.getSystemClipboard().setContent(content);
}
/**
* Formats a message timestamp as {@code [HH:mm:ss]} in the system default zone.
*
* @param instant the source instant; must not be {@code null}
* @return the formatted prefix; never {@code null}
*/
private static String formatTimestamp(java.time.Instant instant) {
return "[" + TIMESTAMP_FORMATTER.format(instant) + "]";
}
private static final java.time.format.DateTimeFormatter TIMESTAMP_FORMATTER =
java.time.format.DateTimeFormatter.ofPattern("HH:mm:ss")
.withZone(java.time.ZoneId.systemDefault());
// =========================================================================
// Field-level error label rendering
// =========================================================================
@@ -145,17 +145,16 @@ public final class GuiTechnicalTestCoordinator {
/**
* Wendet das Ergebnis des vollständigen Gesamttests auf die geteilte Nachrichtenliste an.
* <p>
* Entfernt alle vorherigen Einträge mit Quelle {@link #SOURCE_TAG} und fügt für jeden
* Checkpoint-Ergebnis einen neuen Eintrag hinzu. Zusätzlich wird eine Zusammenfassung
* angehängt.
* Fügt für jedes Checkpoint-Ergebnis einen neuen Eintrag zur geteilten Nachrichtenliste
* hinzu; vorhandene Einträge bleiben erhalten, sodass die Meldungen über mehrere
* Testläufe hinweg akkumulieren. Zusätzlich wird eine Zusammenfassung angehängt.
* <p>
* Muss nur auf dem JavaFX Application Thread aufgerufen werden (via {@code resultDelivery}).
*
* @param report der vollständige Gesamttestbericht; darf nicht {@code null} sein
*/
private void applyResult(TechnicalTestReport report) {
// Alte Einträge mit Source-Tag entfernen (Replace-Semantik)
pendingMessages.removeIf(msg -> SOURCE_TAG.equals(msg.source().orElse("")));
// Akkumulieren: Vorherige Einträge anderer Läufe bleiben erhalten.
long successCount = 0;
long failureErrorCount = 0;
@@ -48,15 +48,28 @@ public final class GuiConfigurationTemplateFactory {
}
/**
* Creates the empty editor state used when the GUI starts without a loaded configuration.
* Creates the editor state used when the GUI starts without a loaded configuration.
* <p>
* This start state intentionally does not show the standard template yet. The template
* is reserved for the explicit {@code Neu} action so the GUI starts without an implicit
* draft and only shows the welcome guidance until the user requests a new configuration.
* The start state contains the standard configuration template so the GUI shows the
* default values immediately, equivalent to the explicit {@code Neu} action having been
* triggered. No file snapshot is associated with the state.
*
* @return a clean editor state without a loaded file snapshot and without template values
* @return a clean editor state with the standard template values and no loaded file snapshot
*/
public static GuiConfigurationEditorState createBlankStartState() {
return createStandardTemplate();
}
/**
* Creates a truly empty editor state without any template values.
* <p>
* This factory is reserved for tests that intentionally need an editor state with empty
* field values and no provider configurations. Production startup uses
* {@link #createBlankStartState()} which returns the standard template instead.
*
* @return a clean editor state without any template values
*/
public static GuiConfigurationEditorState createEmptyStartState() {
GuiConfigurationValues blankValues = new GuiConfigurationValues(
"",
"",
@@ -4,6 +4,7 @@ import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.ComboBox;
import javafx.scene.control.TextField;
@@ -65,6 +66,7 @@ public final class GuiModelFieldContainer extends StackPane {
// Initial state: show text field (NOT_YET_LOADED → manual input)
applyVisibility(false);
setAlignment(Pos.CENTER_LEFT);
getChildren().addAll(comboBox, textField);
}