This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new bfeacf53283d chore: camel-jbang - a launched example runs its source 
as saved, the log opens on a whole line, the circuit breaker chart shows its 
calls, events of one millisecond read newest first, and error counts add up 
(#29486)
bfeacf53283d is described below

commit bfeacf53283d6002cb62da5dd80db83ae344f8b1
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Nov 9 17:11:06 2026 +0100

    chore: camel-jbang - a launched example runs its source as saved, the log 
opens on a whole line, the circuit breaker chart shows its calls, events of one 
millisecond read newest first, and error counts add up (#29486)
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../jbang/core/commands/tui/CircuitBreakerTab.java | 10 ++-
 .../dsl/jbang/core/commands/tui/ErrorsTab.java     | 48 +++++++++++--
 .../dsl/jbang/core/commands/tui/EventTab.java      | 24 +++++--
 .../jbang/core/commands/tui/IntegrationInfo.java   |  2 +
 .../camel/dsl/jbang/core/commands/tui/LogTab.java  | 43 ++++++++++--
 .../jbang/core/commands/tui/MetricsCollector.java  | 21 ++++--
 .../jbang/core/commands/tui/SourceRunState.java    |  5 +-
 .../dsl/jbang/core/commands/tui/StatusParser.java  |  1 +
 .../src/main/resources/tui/help/circuit-breaker.md | 23 ++++---
 .../src/main/resources/tui/help/errors.md          | 12 ++--
 .../commands/tui/CircuitBreakerTabRenderTest.java  | 21 ++++++
 .../core/commands/tui/ErrorsTabRenderTest.java     | 23 +++++++
 .../jbang/core/commands/tui/EventTabOrderTest.java | 56 +++++++++++++++
 .../jbang/core/commands/tui/LogTabTailTest.java    | 80 ++++++++++++++++++++++
 .../tui/MetricsCollectorCircuitBreakerTest.java    | 55 +++++++++++++++
 .../core/commands/tui/SourceRunStateTest.java      | 13 ++++
 16 files changed, 400 insertions(+), 37 deletions(-)

diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CircuitBreakerTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CircuitBreakerTab.java
index 544d980ae293..4550c9580a34 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CircuitBreakerTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CircuitBreakerTab.java
@@ -114,6 +114,10 @@ class CircuitBreakerTab extends AbstractTableTab {
             rows.add(emptyRow("No circuit breakers", 13));
         }
 
+        // a breaker that shows up once the tab is open (the route is still 
starting) is selected like one that was there
+        if (!sorted.isEmpty() && tableState.selected() == null) {
+            tableState.select(0);
+        }
         CircuitBreakerInfo selectedCb = null;
         Integer sel = tableState.selected();
         if (sel != null && sel >= 0 && sel < sorted.size()) {
@@ -335,15 +339,17 @@ class CircuitBreakerTab extends AbstractTableTab {
         long curFail = failArr[renderPoints - 1];
         Line chartTitle = Line.from(
                 Span.styled("▬", Theme.success()),
-                Span.raw(String.format(" ok:%-4d ", curSuccess)),
+                Span.raw(String.format(" ok:%-4s ", 
MetricsCollector.formatThroughput(curSuccess))),
                 Span.styled("▬", Theme.error()),
-                Span.raw(String.format(" fail:%-4d msg/s ", curFail)));
+                Span.raw(String.format(" fallback:%-4s msg/s ", 
MetricsCollector.formatThroughput(curFail))));
         frame.renderWidget(DualSparkline.builder()
                 .topData(successArr)
                 .bottomData(failArr)
                 .topStyle(Theme.success())
                 .bottomStyle(Theme.error())
                 .showYAxis(true)
+                // the rates are scaled by THROUGHPUT_SCALE; the axis shows 
msg/s like the title
+                .yAxisFormatter(MetricsCollector::formatThroughput)
                 .xLabels("-" + renderPoints + "s", "-" + (renderPoints * 3 / 
4) + "s",
                         "-" + (renderPoints / 2) + "s", "-" + (renderPoints / 
4) + "s", "now")
                 
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL).borderStyle(ctx.paneBorder(false))
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ErrorsTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ErrorsTab.java
index 48aeaa8492bf..4a6e16d1b2c3 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ErrorsTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ErrorsTab.java
@@ -18,8 +18,12 @@ package org.apache.camel.dsl.jbang.core.commands.tui;
 
 import java.nio.file.Path;
 import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.IdentityHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 import dev.tamboui.layout.Constraint;
 import dev.tamboui.layout.Layout;
@@ -349,6 +353,7 @@ class ErrorsTab extends AbstractTableTab {
         }
 
         List<ErrorInfo> sorted = applyFilter(info.errors);
+        Set<ErrorInfo> newest = newestOfEachKind(sorted);
 
         List<Row> rows = new ArrayList<>();
         for (ErrorInfo ei : sorted) {
@@ -366,9 +371,7 @@ class ErrorsTab extends AbstractTableTab {
                     Cell.from(Span.styled(ei.routeId != null ? ei.routeId : 
"", Style.EMPTY.fg(Theme.accent()))),
                     Cell.from(ei.nodeId != null ? ei.nodeId : ""),
                     Cell.from(Span.styled(handledStr, handledStyle)),
-                    Cell.from(ei.repeatCount > 1
-                            ? Span.styled(Long.toString(ei.repeatCount), 
Theme.error().bold())
-                            : Span.raw(Long.toString(Math.max(1, 
ei.repeatCount)))),
+                    countCell(ei, newest),
                     ChaosMarks.markedCell(ChaosMarks.ruleOf(ctx, info, 
ei.exchangeId, ei.properties), shortException),
                     Cell.from(ei.exceptionMessage != null ? 
ei.exceptionMessage : "")));
         }
@@ -412,7 +415,7 @@ class ErrorsTab extends AbstractTableTab {
                 .highlightStyle(Theme.selectionBg())
                 .highlightSpacing(Table.HighlightSpacing.ALWAYS)
                 
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL).borderStyle(ctx.paneBorder(true))
-                        .title(" Errors [" + sorted.size() + "] ").build())
+                        .title(title(sorted.size(), info.errorCount)).build())
                 .build();
 
         lastTableArea = chunks.get(0);
@@ -571,6 +574,43 @@ class ErrorsTab extends AbstractTableTab {
                 RuntimeFailures.lineText(file, line));
     }
 
+    /**
+     * The title: how many errors are listed, and how many happened when that 
is more. The registry keeps the newest few
+     * of each kind of error (CAMEL-24911).
+     */
+    static String title(int listed, int happened) {
+        if (happened > listed) {
+            return " Errors [" + listed + "] " + happened + " happened, the 
newest of each kind are kept ";
+        }
+        return " Errors [" + listed + "] ";
+    }
+
+    /**
+     * How often a kind of error happened, on its newest entry: an older entry 
counts the times up to then, so it is
+     * left blank and the column adds up to the errors that happened.
+     */
+    private static Cell countCell(ErrorInfo ei, Set<ErrorInfo> newest) {
+        if (!newest.contains(ei)) {
+            return Cell.from("");
+        }
+        return ei.repeatCount > 1
+                ? Cell.from(Span.styled(Long.toString(ei.repeatCount), 
Theme.error().bold()))
+                : Cell.from(Long.toString(Math.max(1, ei.repeatCount)));
+    }
+
+    /** The newest entry of each kind of error (a route and an exception 
type): the one with the highest count. */
+    static Set<ErrorInfo> newestOfEachKind(List<ErrorInfo> errors) {
+        Map<String, ErrorInfo> newest = new HashMap<>();
+        for (ErrorInfo ei : errors) {
+            newest.merge(ei.routeId + "|" + ei.exceptionType, ei,
+                    (a, b) -> b.repeatCount > a.repeatCount
+                            || b.repeatCount == a.repeatCount && b.timestamp > 
a.timestamp ? b : a);
+        }
+        Set<ErrorInfo> set = Collections.newSetFromMap(new 
IdentityHashMap<>());
+        set.addAll(newest.values());
+        return set;
+    }
+
     /** How often this kind of error happened, and how long it has been going 
on (CAMEL-24911). */
     private static void addRepeatLine(List<Line> lines, ErrorInfo ei) {
         if (ei.repeatCount <= 1) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EventTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EventTab.java
index 04b0b0686beb..91fa539b245d 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EventTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EventTab.java
@@ -17,6 +17,7 @@
 package org.apache.camel.dsl.jbang.core.commands.tui;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 
 import dev.tamboui.layout.Constraint;
@@ -77,8 +78,7 @@ class EventTab extends AbstractTableTab {
 
     @Override
     protected void renderContent(Frame frame, Rect area, IntegrationInfo info) 
{
-        List<EventInfo> sorted = new ArrayList<>(info.events);
-        sorted.sort(this::sortEvent);
+        List<EventInfo> sorted = sortedEvents(info);
 
         List<Row> rows = new ArrayList<>();
         for (EventInfo ei : sorted) {
@@ -117,6 +117,20 @@ class EventTab extends AbstractTableTab {
         renderScrollbar(frame, table, sorted.size());
     }
 
+    /**
+     * The events in the sort order. They come oldest first, and the sort 
keeps the order of equal ones: unless the sort
+     * is reversed, the list is turned around first, so events of the same 
millisecond (the created, sent and completed
+     * events of a quick exchange) read newest first like the others.
+     */
+    List<EventInfo> sortedEvents(IntegrationInfo info) {
+        List<EventInfo> sorted = new ArrayList<>(info.events);
+        if (!sortReversed) {
+            Collections.reverse(sorted);
+        }
+        sorted.sort(this::sortEvent);
+        return sorted;
+    }
+
     private int sortEvent(EventInfo a, EventInfo b) {
         int result = switch (sort) {
             case "category" -> {
@@ -157,8 +171,7 @@ class EventTab extends AbstractTableTab {
         if (info == null || info.events.isEmpty()) {
             return null;
         }
-        List<EventInfo> sorted = new ArrayList<>(info.events);
-        sorted.sort(this::sortEvent);
+        List<EventInfo> sorted = sortedEvents(info);
         List<String> items = sorted.stream().map(e -> e.type != null ? e.type 
: "").toList();
         Integer sel = tableState.selected();
         return new SelectionContext("table", items, sel != null ? sel : -1, 
items.size(), "Events");
@@ -183,8 +196,7 @@ class EventTab extends AbstractTableTab {
         JsonObject result = new JsonObject();
         result.put("tab", "Events");
         JsonArray rows = new JsonArray();
-        List<EventInfo> sorted = new ArrayList<>(info.events);
-        sorted.sort(this::sortEvent);
+        List<EventInfo> sorted = sortedEvents(info);
         for (EventInfo ei : sorted) {
             JsonObject row = new JsonObject();
             row.put("type", ei.type);
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/IntegrationInfo.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/IntegrationInfo.java
index 9df5ad327f7b..9879aa7d6080 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/IntegrationInfo.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/IntegrationInfo.java
@@ -51,6 +51,8 @@ class IntegrationInfo {
     String ready;
     int state;
     long uptime;
+    // when the CamelContext started (epoch millis), after it loaded its 
routes; 0 when the integration does not say
+    long contextStarted;
     String ago;
     String throughput;
     long exchangesTotal;
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LogTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LogTab.java
index f9d7e3b47a11..8442ece204b3 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LogTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LogTab.java
@@ -80,8 +80,13 @@ class LogTab extends AbstractTab {
     private volatile List<LogEntry> filteredLogEntries = new ArrayList<>();
     private volatile boolean logLoading;
     private volatile boolean loadAllRequested;
+    /** How much of the end of a log the tab reads when it opens it. */
+    static final int INITIAL_TAIL_BYTES = 32 * 1024;
+
     private long logFilePos = -1;
     private long logFileStartPos = -1;
+    // the first read of a log starts inside a line: it is skipped up to its 
end
+    private boolean skipPartialLine;
     private long logTotalLinesRead;
     private String logFilePid;
     private final StringBuilder logLineBuffer = new StringBuilder();
@@ -717,7 +722,10 @@ class LogTab extends AbstractTab {
     }
 
     void readNewLogLinesFromFile(String pid, String fileName, List<String> 
newLines) {
-        Path logFile = CommandLineHelper.getCamelDir().resolve(fileName);
+        readNewLogLines(pid, 
CommandLineHelper.getCamelDir().resolve(fileName), newLines);
+    }
+
+    void readNewLogLines(String pid, Path logFile, List<String> newLines) {
         if (!Files.exists(logFile)) {
             logFilePid = pid;
             logFilePos = -1;
@@ -727,10 +735,11 @@ class LogTab extends AbstractTab {
         try (RandomAccessFile raf = new RandomAccessFile(logFile.toFile(), 
"r")) {
             long length = raf.length();
             if (logFilePos < 0 || logFilePos > length) {
-                // Initial load: only read last 8KB for fast first render
-                logFilePos = Math.max(0, length - 8 * 1024);
+                // Initial load: only read the end of the file for a fast 
first render (enough to fill a tall screen)
+                logFilePos = Math.max(0, length - INITIAL_TAIL_BYTES);
                 logFileStartPos = logFilePos;
                 logLineBuffer.setLength(0);
+                skipPartialLine = logFilePos > 0;
             }
             if (logFilePos >= length) {
                 return;
@@ -739,9 +748,21 @@ class LogTab extends AbstractTab {
             // Cap per-tick read to 64KB to avoid blocking the refresh cycle
             byte[] buf = new byte[(int) Math.min(length - logFilePos, 64 * 
1024)];
             raf.readFully(buf);
+            int from = 0;
+            if (skipPartialLine) {
+                from = indexOfNewline(buf) + 1;
+                if (from == 0) {
+                    // still inside the line the read started in
+                    logFilePos += buf.length;
+                    return;
+                }
+                skipPartialLine = false;
+                // the older lines are read up to the start of the first whole 
line
+                logFileStartPos = logFilePos + from;
+            }
             logFilePos += buf.length;
 
-            String chunk = logLineBuffer + new String(buf, 
StandardCharsets.UTF_8);
+            String chunk = logLineBuffer + new String(buf, from, buf.length - 
from, StandardCharsets.UTF_8);
             logLineBuffer.setLength(0);
             int start = 0;
             int end;
@@ -760,11 +781,23 @@ class LogTab extends AbstractTab {
         }
     }
 
+    private static int indexOfNewline(byte[] buf) {
+        for (int i = 0; i < buf.length; i++) {
+            if (buf[i] == '\n') {
+                return i;
+            }
+        }
+        return -1;
+    }
+
     void readOlderLogLines(String fileName, boolean loadAll, List<String> 
olderLines) {
+        readOlderLogLines(CommandLineHelper.getCamelDir().resolve(fileName), 
loadAll, olderLines);
+    }
+
+    void readOlderLogLines(Path logFile, boolean loadAll, List<String> 
olderLines) {
         if (logFilePid == null || logFileStartPos <= 0) {
             return;
         }
-        Path logFile = CommandLineHelper.getCamelDir().resolve(fileName);
         if (!Files.exists(logFile)) {
             return;
         }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MetricsCollector.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MetricsCollector.java
index 18bbcd30847e..6f555bccd66c 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MetricsCollector.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MetricsCollector.java
@@ -412,15 +412,26 @@ class MetricsCollector {
                 continue;
             }
             String key = info.pid + "/" + cb.id;
-            long success = cb.successfulCalls;
-            long failed = cb.failedCalls;
-            // Circuit breaker history stays unscaled (scale=1) because 
CircuitBreakerTab
-            // formats values as plain integers, not via formatThroughput()
+            long[] calls = cbCalls(cb);
+            long success = calls[0];
+            long failed = calls[1];
+            // scaled by THROUGHPUT_SCALE like the endpoint rates: in whole 
calls a second, a breaker called once a
+            // second reads 0 when the samples are a little more than a second 
apart
             recordEndpointSample(key, now, success, failed,
-                    cbThroughputSamples, previousCbTime, cbSuccessHistory, 
cbFailHistory, 1);
+                    cbThroughputSamples, previousCbTime, cbSuccessHistory, 
cbFailHistory, THROUGHPUT_SCALE);
         }
     }
 
+    /**
+     * The calls of a circuit breaker so far, as running totals: answered by 
the protected steps, and by the fallback (a
+     * failed, timed out or rejected call). The success and failed calls the 
breaker reports are those of its sliding
+     * window, which stay the same while the calls go on.
+     */
+    static long[] cbCalls(CircuitBreakerInfo cb) {
+        long fallback = cb.fallbackCalls;
+        return new long[] { Math.max(0, cb.total - fallback), fallback };
+    }
+
     void updateHeapHistory(IntegrationInfo info) {
         if (info.heapMemUsed > 0) {
             long now = MonotonicClock.millis();
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceRunState.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceRunState.java
index e038144d3741..b338bb0e547d 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceRunState.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceRunState.java
@@ -95,6 +95,9 @@ final class SourceRunState {
         }
         // an integration that has not reloaded loaded its routes when it 
started; after a reload, when each started
         boolean reloaded = info.reloaded > 0 || info.reloadError != null || 
info.uptime <= 0;
+        // the routes were loaded before the context started, but after the 
JVM did: an example run from the TUI
+        // writes its files from the integration itself, after its JVM started
+        long loaded = info.contextStarted > 0 ? info.contextStarted : 
info.uptime;
         int routes = 0;
         int started = 0;
         int stale = 0;
@@ -112,7 +115,7 @@ final class SourceRunState {
             if (reloaded && uptime < 0) {
                 // a stopped route after a reload: when it was loaded is not 
known, so it may be of another version
                 unknown++;
-            } else if ((reloaded ? Math.max(info.uptime, now - uptime) : 
info.uptime) < fileModified) {
+            } else if ((reloaded ? Math.max(loaded, now - uptime) : loaded) < 
fileModified) {
                 stale++;
             }
         }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusParser.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusParser.java
index 99076064e21b..84b106f76216 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusParser.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusParser.java
@@ -68,6 +68,7 @@ final class StatusParser {
         info.pid = Long.toString(ph.pid());
         info.uptime = extractSince(ph);
         info.ago = TimeUtils.printSince(info.uptime);
+        info.contextStarted = context.getLongOrDefault("startTimestamp", 0L);
         info.state = context.getIntegerOrDefault("phase", 0);
         info.camelVersion = context.getString("version");
         info.profile = context.getString("profile");
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/circuit-breaker.md
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/circuit-breaker.md
index 2f167fa7117e..797ec6646829 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/circuit-breaker.md
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/circuit-breaker.md
@@ -44,8 +44,8 @@ CLOSED ──(failures exceed threshold)──> OPEN
 - **STATE** — Current breaker state: `CLOSED` (green, normal), `OPEN` (red, 
rejecting), or `HALF_OPEN` (yellow, testing)
 - **WINDOW** — Number of calls in the current sliding window used to calculate 
the failure rate
 - **INFLIGHT** — Calls currently in progress inside the circuit breaker
-- **SUCCESS** — Total number of successful calls
-- **FAIL** — Total number of failed calls (exceptions thrown by the protected 
code)
+- **SUCCESS** — Successful calls in the sliding window
+- **FAIL** — Failed calls in the sliding window (exceptions thrown by the 
protected code)
 - **RATE%** — Current failure rate percentage in the sliding window. When this 
exceeds the configured threshold, the circuit trips to OPEN
 - **REJECT** — Calls rejected because the circuit is OPEN. These calls never 
reach the downstream service — they fail fast with a fallback
 - **FALLBACK** — Calls answered by the `onFallback`, whatever the cause: a 
failed call, a timeout or a rejected call. This is how many callers got a 
degraded answer
@@ -76,13 +76,15 @@ HALF_OPEN state.
 
 ## Reading the counters
 
-- FAIL counts calls the protected code failed, timeouts included.
-  TIMEOUT tells how many of those were timeouts.
+- SUCCESS and FAIL are the calls of the sliding window, which the
+  breaker keeps to work out RATE%: they stop growing once the window
+  is full. FAIL includes the timeouts; TIMEOUT tells how many of those
+  were timeouts.
 - REJECT counts calls the OPEN breaker refused. They are not in FAIL.
-- FALLBACK counts every call the `onFallback` answered, so it is
-  normally FAIL plus REJECT plus the bulkhead rejections, as long as
-  the breaker has a fallback. Without a fallback it stays at zero and
-  the exceptions reach the route instead.
+- FALLBACK is a running total: every call the `onFallback` answered,
+  whether it failed, timed out or was rejected, as long as the breaker
+  has a fallback. Without a fallback it stays at zero and the
+  exceptions reach the route instead.
 - Calls rejected by a full bulkhead are not in REJECT; they appear as
   `bulkhead` in the detail view.
 
@@ -92,8 +94,9 @@ The bottom panel shows when a circuit breaker is selected:
 
 - **Failure rate gauge**: Visual bar from 0% to 100%, colored green
   (low risk), yellow (approaching threshold), or red (above threshold)
-- **Sparkline chart**: Mirrored view showing successful calls (green,
-  upward) vs failed calls (red, downward) over time
+- **Sparkline chart**: Mirrored view of the calls per second: those the
+  protected steps answered (green, upward) and those the fallback answered
+  (red, downward), whether the call failed, timed out or was rejected
 - **Metrics**: Detailed counts for total, fail, inflight, reject,
   fallback, timeout, bulkhead (calls rejected because the bulkhead
   was full) and timing statistics (mean/min/max processing time)
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/errors.md 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/errors.md
index 48b84e796c02..aa22d1fdf428 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/errors.md
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/errors.md
@@ -6,7 +6,9 @@ exception, the exchange state at the time of failure, and the 
full path
 the message took through the route before it failed.
 
 Camel maintains an error registry that keeps the most recent errors
-(up to a configurable limit). Older errors are automatically purged.
+(up to a configurable limit), and only the newest few of each kind of
+error, so a storm of one failure does not push the others out. When
+more errors happened than are kept, the title says how many happened.
 
 ## Error List
 
@@ -15,15 +17,17 @@ Camel maintains an error registry that keeps the most 
recent errors
 - **ROUTE** — Route where the error happened
 - **NODE** — The specific processor/node in the route where the exception was 
thrown (e.g., `to1`, `bean2`, `process3`). This tells you exactly which step 
failed
 - **HANDLED** — Whether the error was caught by Camel's error handling: `true` 
(handled) or `false` (unhandled)
+- **COUNT** — How often this kind of error (the same exception in the same 
route) happened, on its newest entry. The older entries of the kind are left 
blank, so the column adds up to the errors that happened
 - **EXCEPTION** — Exception class name in short form (e.g., `IOException`, 
`HttpOperationFailedException`)
 - **MESSAGE** — The exception message text explaining what went wrong
 
 ## Example Screen
 
 ```
- ID                  AGO  ROUTE        NODE   HANDLED  EXCEPTION    MESSAGE
- ID-myhost-1234-5    5s   timer-route  to1    false    IOException  Connection 
refused
- ID-myhost-1234-3    2m   kafka-route  bean2  true     NPE          null 
reference in processor
+ ID                  AGO  ROUTE        NODE   HANDLED  COUNT  EXCEPTION    
MESSAGE
+ ID-myhost-1234-5    5s   timer-route  to1    false    4      IOException  
Connection refused
+ ID-myhost-1234-4    9s   timer-route  to1    false           IOException  
Connection refused
+ ID-myhost-1234-3    2m   kafka-route  bean2  true     1      NPE          
null reference in processor
 ```
 
 ## Understanding HANDLED
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/CircuitBreakerTabRenderTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/CircuitBreakerTabRenderTest.java
index 203d787acc2a..ee03b8881f26 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/CircuitBreakerTabRenderTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/CircuitBreakerTabRenderTest.java
@@ -82,6 +82,8 @@ class CircuitBreakerTabRenderTest {
     @Test
     void renderRouteIdInCyan() {
         addCircuitBreaker("my-route", "cb1", "resilience4j", "closed");
+        // the first row is selected, and its highlight takes the colors: the 
row looked at is the second
+        addCircuitBreaker("a-first", "cb0", "resilience4j", "forced_closed");
 
         CircuitBreakerTab tab = new CircuitBreakerTab(ctx, new 
MetricsCollector());
 
@@ -97,6 +99,8 @@ class CircuitBreakerTabRenderTest {
     @Test
     void renderClosedStateInGreen() {
         addCircuitBreaker("route1", "cb1", "resilience4j", "closed");
+        // the first row is selected, and its highlight takes the colors: the 
row looked at is the second
+        addCircuitBreaker("a-first", "cb0", "resilience4j", "forced_closed");
 
         CircuitBreakerTab tab = new CircuitBreakerTab(ctx, new 
MetricsCollector());
 
@@ -112,6 +116,8 @@ class CircuitBreakerTabRenderTest {
     @Test
     void renderOpenStateInRed() {
         addCircuitBreaker("route1", "cb1", "resilience4j", "open");
+        // the first row is selected, and its highlight takes the colors: the 
row looked at is the second
+        addCircuitBreaker("a-first", "cb0", "resilience4j", "forced_closed");
 
         CircuitBreakerTab tab = new CircuitBreakerTab(ctx, new 
MetricsCollector());
 
@@ -127,6 +133,8 @@ class CircuitBreakerTabRenderTest {
     @Test
     void renderHalfOpenStateInYellow() {
         addCircuitBreaker("route1", "cb1", "resilience4j", "half_open");
+        // the first row is selected, and its highlight takes the colors: the 
row looked at is the second
+        addCircuitBreaker("a-first", "cb0", "resilience4j", "forced_closed");
 
         CircuitBreakerTab tab = new CircuitBreakerTab(ctx, new 
MetricsCollector());
 
@@ -161,6 +169,19 @@ class CircuitBreakerTabRenderTest {
                 "Should show placeholder when no circuit breakers exist");
     }
 
+    @Test
+    void aBreakerThatShowsUpOnceTheTabIsOpenIsSelected() {
+        CircuitBreakerTab tab = new CircuitBreakerTab(ctx, new 
MetricsCollector());
+        tab.onTabSelected();
+        TuiTestHelper.renderToString(tab, 160, 40);
+
+        // the route was still starting: the breaker shows up a refresh later
+        addCircuitBreaker("route1", "cb1", "resilience4j", "closed");
+        String rendered = TuiTestHelper.renderToString(tab, 160, 40);
+
+        assertTrue(rendered.contains("cb1 (resilience4j)"), "the detail of the 
breaker shows: " + rendered);
+    }
+
     @Test
     void renderNoSelectionShowsPrompt() {
         ctx.selectedPid = null;
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ErrorsTabRenderTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ErrorsTabRenderTest.java
index 32068c8da965..f102251cebe1 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ErrorsTabRenderTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ErrorsTabRenderTest.java
@@ -18,6 +18,7 @@ package org.apache.camel.dsl.jbang.core.commands.tui;
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Set;
 import java.util.concurrent.atomic.AtomicReference;
 
 import dev.tamboui.buffer.Buffer;
@@ -195,6 +196,28 @@ class ErrorsTabRenderTest {
         String rendered = renderToString(tab, 160, 30);
 
         assertTrue(rendered.contains("Errors [2]"), "Title should show error 
count");
+        assertFalse(rendered.contains("happened"), "No more errors happened 
than are listed");
+    }
+
+    @Test
+    void theCountIsOnTheNewestEntryOfAKindAndTheTitleSaysHowManyHappened() {
+        // the registry keeps the newest 3 of each kind: 3 of 3 connection 
failures, and the 8th to 10th rejection
+        for (int i = 1; i <= 3; i++) {
+            addError("ID-00" + i, "supplier", "throwException1", 
"ConnectException", "supplier unreachable", true);
+            info.errors.get(info.errors.size() - 1).repeatCount = i;
+        }
+        for (int i = 8; i <= 10; i++) {
+            addError("ID-01" + i, "stock-check", "setBody1", 
"CallNotPermittedException", "OPEN", true);
+            info.errors.get(info.errors.size() - 1).repeatCount = i;
+        }
+        info.errorCount = 13;
+
+        assertEquals(Set.of(info.errors.get(2), info.errors.get(5)), 
ErrorsTab.newestOfEachKind(info.errors));
+
+        String rendered = renderToString(new ErrorsTab(ctx), 160, 30);
+        assertTrue(rendered.contains("Errors [6] 13 happened, the newest of 
each kind are kept"), rendered);
+        assertFalse(rendered.contains(" 8 "), "the older entries of a kind 
leave COUNT blank");
+        assertTrue(rendered.contains("10"));
     }
 
     @Test
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/EventTabOrderTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/EventTabOrderTest.java
new file mode 100644
index 000000000000..617ccf94b60b
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/EventTabOrderTest.java
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * The events of a quick exchange share a millisecond: they read newest first 
like the others.
+ */
+class EventTabOrderTest {
+
+    @Test
+    void eventsOfTheSameMillisecondReadNewestFirst() {
+        IntegrationInfo info = new IntegrationInfo();
+        // as the integration reports them: oldest first
+        info.events.add(event("ExchangeCreated", 1000));
+        info.events.add(event("ExchangeSending", 1000));
+        info.events.add(event("ExchangeSent", 1000));
+        info.events.add(event("ExchangeCompleted", 1000));
+        info.events.add(event("ExchangeCreated", 2000));
+        info.events.add(event("ExchangeCompleted", 2000));
+
+        EventTab tab = new EventTab(new MonitorContext(new 
AtomicReference<>(List.of(info)), new AtomicReference<>(List.of())));
+
+        assertThat(tab.sortedEvents(info)).extracting(e -> e.type + "@" + 
e.timestamp).containsExactly(
+                "ExchangeCompleted@2000", "ExchangeCreated@2000",
+                "ExchangeCompleted@1000", "ExchangeSent@1000", 
"ExchangeSending@1000", "ExchangeCreated@1000");
+    }
+
+    private static EventInfo event(String type, long timestamp) {
+        EventInfo e = new EventInfo();
+        e.type = type;
+        e.category = "exchange";
+        e.timestamp = timestamp;
+        return e;
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/LogTabTailTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/LogTabTailTest.java
new file mode 100644
index 000000000000..bd030d623ae3
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/LogTabTailTest.java
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * The Log tab opens a log at its end, which starts inside a line: that line 
is left out of the first read, and comes
+ * back whole with the older lines.
+ */
+class LogTabTailTest {
+
+    @TempDir
+    Path dir;
+
+    @Test
+    void theFirstReadStartsAtAWholeLine() throws Exception {
+        List<String> written = new ArrayList<>();
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; sb.length() < 3 * LogTab.INITIAL_TAIL_BYTES; i++) {
+            String line = "2026-11-09 15:13:22.234  INFO 292054 --- 
[timer://stock] route : Stock check " + i
+                          + " é in stock at the supplier";
+            written.add(line);
+            sb.append(line).append('\n');
+        }
+        Path log = dir.resolve("1234.log");
+        Files.writeString(log, sb, StandardCharsets.UTF_8);
+
+        LogTab tab = new LogTab(new MonitorContext(new 
AtomicReference<>(List.of()), new AtomicReference<>(List.of())));
+        List<String> lines = new ArrayList<>();
+        tab.readNewLogLines("1234", log, lines);
+
+        assertThat(lines).isNotEmpty();
+        assertThat(written).containsSequence(lines);
+        assertThat(lines.get(lines.size() - 
1)).isEqualTo(written.get(written.size() - 1));
+
+        // the older lines end with the line just before the first one read: 
none is lost or cut
+        List<String> older = new ArrayList<>();
+        tab.readOlderLogLines(log, true, older);
+        List<String> all = new ArrayList<>(older);
+        all.addAll(lines);
+        assertThat(all).isEqualTo(written);
+    }
+
+    @Test
+    void aShortLogIsReadWhole() throws Exception {
+        Path log = dir.resolve("1234.log");
+        Files.writeString(log, "first\nsecond\n", StandardCharsets.UTF_8);
+
+        LogTab tab = new LogTab(new MonitorContext(new 
AtomicReference<>(List.of()), new AtomicReference<>(List.of())));
+        List<String> lines = new ArrayList<>();
+        tab.readNewLogLines("1234", log, lines);
+
+        assertThat(lines).containsExactly("first", "second");
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/MetricsCollectorCircuitBreakerTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/MetricsCollectorCircuitBreakerTest.java
new file mode 100644
index 000000000000..2569f8584ab8
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/MetricsCollectorCircuitBreakerTest.java
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * The calls per second of the Circuit Breaker chart come from running totals, 
not from the sliding window of the
+ * breaker, which stays the same while the calls go on.
+ */
+class MetricsCollectorCircuitBreakerTest {
+
+    @Test
+    void theCallsAreRunningTotalsNotTheSlidingWindow() {
+        CircuitBreakerInfo cb = new CircuitBreakerInfo();
+        // a window of 4, full of successful calls, after 5947 calls of which 
the fallback answered 13
+        cb.bufferedCalls = 4;
+        cb.successfulCalls = 4;
+        cb.total = 5947;
+        cb.fallbackCalls = 13;
+
+        assertThat(MetricsCollector.cbCalls(cb)).containsExactly(5934, 13);
+
+        // a second later the window is the same, the totals have moved on
+        cb.total = 5948;
+        assertThat(MetricsCollector.cbCalls(cb)).containsExactly(5935, 13);
+    }
+
+    @Test
+    void anOutageIsAnsweredByTheFallback() {
+        CircuitBreakerInfo cb = new CircuitBreakerInfo();
+        cb.total = 100;
+        cb.fallbackCalls = 10;
+        cb.total += 5;
+        cb.fallbackCalls += 5;
+
+        assertThat(MetricsCollector.cbCalls(cb)).containsExactly(90, 15);
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceRunStateTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceRunStateTest.java
index 983dd5d9ba97..88c82c85a389 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceRunStateTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceRunStateTest.java
@@ -66,6 +66,19 @@ class SourceRunStateTest {
         
assertThat(text(SourceRunState.line(state))).isEqualTo(TuiIcons.SELECTED + " 
running as saved");
     }
 
+    @Test
+    void writtenByTheIntegrationBeforeItsContextStarted() {
+        // an example run from the TUI writes its files from the integration 
itself: after the JVM started, before
+        // the routes were loaded and the context started
+        info.contextStarted = NOW - 58_000;
+        route("orders", "orders.camel.yaml:3", "57s");
+
+        assertThat(SourceRunState.of(info, FILE, NOW - 59_000, 
NOW).kind()).isEqualTo(SourceRunState.Kind.RUNNING);
+        // saved after the context started: not what runs
+        assertThat(SourceRunState.of(info, FILE, NOW - 10_000, NOW).kind())
+                .isEqualTo(SourceRunState.Kind.NOT_RUNNING);
+    }
+
     @Test
     void savedAfterTheIntegrationStartedWithoutDevMode() {
         route("orders", "orders.camel.yaml:3", "59s");

Reply via email to