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

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

commit 86a068a2acd1797a1842e7e03c9ebe72ae502032
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue May 26 21:45:05 2026 +0200

    CAMEL-23618: camel-tui - Add payload size metrics to Endpoints tab
    
    Display body and header size columns (min/max/mean) in the Endpoints table
    when message size tracking is enabled. Add a mirrored sparkline chart 
showing
    IN vs OUT average body sizes over time. Add Reset Stats action to the F2 
menu
    that resets route counters, endpoint registry stats, and all local sparkline
    history. Rename consumer TOTAL column to POLLS to clarify it tracks 
scheduler
    poll count, not exchange totals.
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
---
 .../camel/cli/connector/LocalCliConnector.java     |   5 +
 .../dsl/jbang/core/commands/tui/ActionsPopup.java  |  17 +-
 .../dsl/jbang/core/commands/tui/CamelMonitor.java  |  74 ++++++++-
 .../dsl/jbang/core/commands/tui/ConsumersTab.java  |   6 +-
 .../dsl/jbang/core/commands/tui/EndpointInfo.java  |   6 +
 .../dsl/jbang/core/commands/tui/EndpointsTab.java  | 180 +++++++++++++++++----
 .../jbang/core/commands/tui/MirroredSparkline.java |  34 +++-
 7 files changed, 277 insertions(+), 45 deletions(-)

diff --git 
a/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
 
b/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
index bb5b0506492d..f32121763448 100644
--- 
a/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
+++ 
b/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
@@ -74,6 +74,7 @@ import org.apache.camel.spi.Resource;
 import org.apache.camel.spi.ResourceLoader;
 import org.apache.camel.spi.ResourceReloadStrategy;
 import org.apache.camel.spi.RoutesLoader;
+import org.apache.camel.spi.RuntimeEndpointRegistry;
 import org.apache.camel.spi.ShutdownPrepared;
 import org.apache.camel.support.LoadOnDemandReloadStrategy;
 import org.apache.camel.support.MessageHelper;
@@ -918,6 +919,10 @@ public class LocalCliConnector extends ServiceSupport 
implements CliConnector, C
         if (mcc != null) {
             mcc.getManagedCamelContext().reset(true);
         }
+        RuntimeEndpointRegistry reg = 
camelContext.getRuntimeEndpointRegistry();
+        if (reg != null) {
+            reg.reset();
+        }
     }
 
     private void doActionDebugTask(JsonObject root) throws Exception {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
index a5050d6cc395..28c6eed536a4 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
@@ -66,7 +66,8 @@ class ActionsPopup {
     private static final int ACTION_CLASSPATH = 8;
     private static final int ACTION_MCP_INFO = 9;
     private static final int ACTION_MCP_LOG = 10;
-    private static final int ACTION_STOP_ALL = 11;
+    private static final int ACTION_RESET_STATS = 11;
+    private static final int ACTION_STOP_ALL = 12;
 
     private final Supplier<Set<String>> runningNames;
     private final Supplier<List<IntegrationInfo>> integrations;
@@ -74,6 +75,7 @@ class ActionsPopup {
     private final Runnable toggleKeystrokes;
     private final Supplier<Boolean> keystrokesEnabled;
     private final Runnable toggleTapeRecording;
+    private Runnable resetStatsAction;
     private final Supplier<Boolean> tapeRecordingActive;
     private MonitorContext ctx;
     private boolean mcpEnabled;
@@ -133,6 +135,10 @@ class ActionsPopup {
         this.ctx = ctx;
     }
 
+    void setResetStatsAction(Runnable resetStatsAction) {
+        this.resetStatsAction = resetStatsAction;
+    }
+
     void setMcpEnabled(
             boolean enabled, int port, Supplier<String> connectedClient, 
Supplier<List<TuiMcpServer.LogEntry>> activityLog) {
         this.mcpEnabled = enabled;
@@ -143,7 +149,7 @@ class ActionsPopup {
     }
 
     private int actionCount() {
-        return mcpEnabled ? 12 : 10;
+        return mcpEnabled ? 13 : 11;
     }
 
     boolean isVisible() {
@@ -195,6 +201,7 @@ class ActionsPopup {
         labels.add("Tape Recording Guide");
         labels.add("Run Doctor");
         labels.add("Show Classpath");
+        labels.add("Reset Stats");
         if (mcpEnabled) {
             labels.add("MCP Info");
             labels.add("MCP Log");
@@ -348,6 +355,11 @@ class ActionsPopup {
                     } else if (action == ACTION_MCP_LOG) {
                         showActionsMenu = false;
                         openMcpLog();
+                    } else if (action == ACTION_RESET_STATS) {
+                        showActionsMenu = false;
+                        if (resetStatsAction != null) {
+                            resetStatsAction.run();
+                        }
                     } else if (action == ACTION_STOP_ALL) {
                         showActionsMenu = false;
                         stopAllPopup.open();
@@ -485,6 +497,7 @@ class ActionsPopup {
         items.add(ListItem.from("  📄 Tape Recording Guide"));
         items.add(ListItem.from("  🩺 Run Doctor"));
         items.add(ListItem.from("  📦 Show Classpath"));
+        items.add(ListItem.from("  🔄 Reset Stats"));
         if (mcpEnabled) {
             items.add(ListItem.from("  🤖 MCP Info"));
             items.add(ListItem.from("  📋 MCP Log"));
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
index bbe0e04ec8b8..99df0ca90dc7 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
@@ -179,6 +179,11 @@ public class CamelMonitor extends CamelCommand {
     private final Map<String, LinkedList<long[]>> endpointRemoteStubSamples = 
new ConcurrentHashMap<>();
     private final Map<String, Long> previousEndpointRemoteStubTime = new 
ConcurrentHashMap<>();
 
+    // Endpoint payload size (mean body size) history per PID — for sparkline
+    private final Map<String, LinkedList<Long>> endpointInSizeHistory = new 
ConcurrentHashMap<>();
+    private final Map<String, LinkedList<Long>> endpointOutSizeHistory = new 
ConcurrentHashMap<>();
+    private final Map<String, Long> previousEndpointSizeTime = new 
ConcurrentHashMap<>();
+
     // Circuit breaker throughput history per PID/cbId (success + fail, one 
point per second)
     private final Map<String, LinkedList<Long>> cbSuccessHistory = new 
ConcurrentHashMap<>();
     private final Map<String, LinkedList<Long>> cbFailHistory = new 
ConcurrentHashMap<>();
@@ -285,6 +290,7 @@ public class CamelMonitor extends CamelCommand {
         // Create shared context and tab instances
         ctx = new MonitorContext(data, infraData);
         actionsPopup.setContext(ctx);
+        actionsPopup.setResetStatsAction(this::resetStats);
         logTab = new LogTab(ctx);
         routesTab = new RoutesTab(ctx);
         consumersTab = new ConsumersTab(ctx);
@@ -292,7 +298,8 @@ public class CamelMonitor extends CamelCommand {
                 ctx,
                 endpointInHistory, endpointOutHistory,
                 endpointRemoteInHistory, endpointRemoteOutHistory,
-                endpointRemoteStubInHistory, endpointRemoteStubOutHistory);
+                endpointRemoteStubInHistory, endpointRemoteStubOutHistory,
+                endpointInSizeHistory, endpointOutSizeHistory);
         httpTab = new HttpTab(ctx);
         healthTab = new HealthTab(ctx);
         historyTab = new HistoryTab(ctx, traces, traceFilePositions);
@@ -1610,6 +1617,39 @@ public class CamelMonitor extends CamelCommand {
         }
     }
 
+    private void resetStats() {
+        IntegrationInfo info = ctx.findSelectedIntegration();
+        if (info == null) {
+            return;
+        }
+        String pid = info.pid;
+        JsonObject root = new JsonObject();
+        root.put("action", "reset-stats");
+        Path actionFile = ctx.getActionFile(pid);
+        PathUtils.writeTextSafely(root.toJson(), actionFile);
+        // Clear local sparkline history — overview
+        throughputHistory.remove(pid);
+        failedHistory.remove(pid);
+        throughputSamples.remove(pid);
+        previousExchangesTime.remove(pid);
+        // Clear local sparkline history — endpoints
+        endpointInHistory.remove(pid);
+        endpointOutHistory.remove(pid);
+        endpointSamples.remove(pid);
+        previousEndpointTime.remove(pid);
+        endpointRemoteInHistory.remove(pid);
+        endpointRemoteOutHistory.remove(pid);
+        endpointRemoteSamples.remove(pid);
+        previousEndpointRemoteTime.remove(pid);
+        endpointRemoteStubInHistory.remove(pid);
+        endpointRemoteStubOutHistory.remove(pid);
+        endpointRemoteStubSamples.remove(pid);
+        previousEndpointRemoteStubTime.remove(pid);
+        endpointInSizeHistory.remove(pid);
+        endpointOutSizeHistory.remove(pid);
+        previousEndpointSizeTime.remove(pid);
+    }
+
     private void sendRouteCommand(String pid, String routeId, String command) {
         JsonObject root = new JsonObject();
         root.put("action", "route");
@@ -1840,6 +1880,10 @@ public class CamelMonitor extends CamelCommand {
                     endpointRemoteStubInHistory.remove(entry.getKey());
                     endpointRemoteStubOutHistory.remove(entry.getKey());
                     endpointRemoteStubSamples.remove(entry.getKey());
+
+                    endpointInSizeHistory.remove(entry.getKey());
+                    endpointOutSizeHistory.remove(entry.getKey());
+                    previousEndpointSizeTime.remove(entry.getKey());
                     previousEndpointRemoteStubTime.remove(entry.getKey());
                     cpuLoadAvg.remove(entry.getKey());
                     prevCpuSample.remove(entry.getKey());
@@ -2097,6 +2141,28 @@ public class CamelMonitor extends CamelCommand {
         recordEndpointSample(pid, now, inRemoteStub, outRemoteStub,
                 endpointRemoteStubSamples, previousEndpointRemoteStubTime,
                 endpointRemoteStubInHistory, endpointRemoteStubOutHistory);
+
+        // Record payload size snapshots (mean body size per direction)
+        long inMeanSize = info.endpoints.stream()
+                .filter(ep -> "in".equals(ep.direction) && ep.meanBodySize >= 
0)
+                .mapToLong(ep -> ep.meanBodySize).max().orElse(0);
+        long outMeanSize = info.endpoints.stream()
+                .filter(ep -> "out".equals(ep.direction) && ep.meanBodySize >= 
0)
+                .mapToLong(ep -> ep.meanBodySize).max().orElse(0);
+        Long lastSizeTime = previousEndpointSizeTime.get(pid);
+        if (lastSizeTime == null || now - lastSizeTime >= 1000) {
+            previousEndpointSizeTime.put(pid, now);
+            LinkedList<Long> inSizeHist = 
endpointInSizeHistory.computeIfAbsent(pid, k -> new LinkedList<>());
+            inSizeHist.add(inMeanSize);
+            while (inSizeHist.size() > MAX_ENDPOINT_CHART_POINTS) {
+                inSizeHist.remove(0);
+            }
+            LinkedList<Long> outSizeHist = 
endpointOutSizeHistory.computeIfAbsent(pid, k -> new LinkedList<>());
+            outSizeHist.add(outMeanSize);
+            while (outSizeHist.size() > MAX_ENDPOINT_CHART_POINTS) {
+                outSizeHist.remove(0);
+            }
+        }
     }
 
     private void recordEndpointSample(
@@ -2844,6 +2910,12 @@ public class CamelMonitor extends CamelCommand {
                     ep.hits = TuiHelper.objToLong(ej.get("hits"));
                     ep.stub = Boolean.TRUE.equals(ej.get("stub"));
                     ep.remote = !Boolean.FALSE.equals(ej.get("remote"));
+                    ep.minBodySize = 
TuiHelper.objToLong(ej.get("minBodySize"));
+                    ep.maxBodySize = 
TuiHelper.objToLong(ej.get("maxBodySize"));
+                    ep.meanBodySize = 
TuiHelper.objToLong(ej.get("meanBodySize"));
+                    ep.minHeadersSize = 
TuiHelper.objToLong(ej.get("minHeadersSize"));
+                    ep.maxHeadersSize = 
TuiHelper.objToLong(ej.get("maxHeadersSize"));
+                    ep.meanHeadersSize = 
TuiHelper.objToLong(ej.get("meanHeadersSize"));
                     // Extract component from URI (e.g., "timer://tick" -> 
"timer")
                     if (ep.uri != null) {
                         int idx = ep.uri.indexOf(':');
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumersTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumersTab.java
index 193010821a16..fc2ea985d452 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumersTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumersTab.java
@@ -37,7 +37,7 @@ import static 
org.apache.camel.dsl.jbang.core.commands.tui.MonitorContext.*;
 
 class ConsumersTab implements MonitorTab {
 
-    private static final String[] SORT_COLUMNS = { "id", "status", "type", 
"inflight", "total", "uri" };
+    private static final String[] SORT_COLUMNS = { "id", "status", "type", 
"inflight", "polls", "uri" };
 
     private final MonitorContext ctx;
     private final TableState tableState = new TableState();
@@ -131,7 +131,7 @@ class ConsumersTab implements MonitorTab {
                         Cell.from(Span.styled(sortLabel("STATUS", "status"), 
sortStyle("status"))),
                         Cell.from(Span.styled(sortLabel("TYPE", "type"), 
sortStyle("type"))),
                         rightCell(sortLabel("INFLIGHT", "inflight"), 8, 
sortStyle("inflight")),
-                        rightCell(sortLabel("TOTAL", "total"), 8, 
sortStyle("total")),
+                        rightCell(sortLabel("POLLS", "polls"), 8, 
sortStyle("polls")),
                         rightCell("PERIOD", 10, Style.EMPTY.bold()),
                         Cell.from(Span.styled("SINCE-LAST", 
Style.EMPTY.bold())),
                         Cell.from(Span.styled(sortLabel("URI", "uri"), 
sortStyle("uri")))))
@@ -179,7 +179,7 @@ class ConsumersTab implements MonitorTab {
                 yield ta.compareToIgnoreCase(tb);
             }
             case "inflight" -> Integer.compare(b.inflight, a.inflight);
-            case "total" -> {
+            case "polls" -> {
                 long la = a.totalCounter != null ? a.totalCounter : 0;
                 long lb = b.totalCounter != null ? b.totalCounter : 0;
                 yield Long.compare(lb, la);
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointInfo.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointInfo.java
index 5f7896933468..21bef41c3e1c 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointInfo.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointInfo.java
@@ -24,4 +24,10 @@ class EndpointInfo {
     long hits;
     boolean stub;
     boolean remote;
+    long minBodySize = -1;
+    long maxBodySize = -1;
+    long meanBodySize = -1;
+    long minHeadersSize = -1;
+    long maxHeadersSize = -1;
+    long meanHeadersSize = -1;
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java
index acbb2d2ea975..4c5baa4909c3 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java
@@ -19,6 +19,7 @@ package org.apache.camel.dsl.jbang.core.commands.tui;
 import java.util.ArrayList;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 
 import dev.tamboui.layout.Constraint;
@@ -45,7 +46,7 @@ import static 
org.apache.camel.dsl.jbang.core.commands.tui.MonitorContext.*;
 
 class EndpointsTab implements MonitorTab {
 
-    private static final String[] SORT_COLUMNS = { "component", "route", 
"dir", "total", "uri" };
+    private static final String[] SORT_COLUMNS = { "component", "route", 
"dir", "total", "body", "hdr", "uri" };
     private static final int MAX_CHART_POINTS = 60;
 
     private final MonitorContext ctx;
@@ -56,6 +57,8 @@ class EndpointsTab implements MonitorTab {
     private final Map<String, LinkedList<Long>> endpointRemoteOutHistory;
     private final Map<String, LinkedList<Long>> endpointRemoteStubInHistory;
     private final Map<String, LinkedList<Long>> endpointRemoteStubOutHistory;
+    private final Map<String, LinkedList<Long>> endpointInSizeHistory;
+    private final Map<String, LinkedList<Long>> endpointOutSizeHistory;
 
     private String sort = "route";
     private int sortIndex = 1;
@@ -69,7 +72,9 @@ class EndpointsTab implements MonitorTab {
                  Map<String, LinkedList<Long>> endpointRemoteInHistory,
                  Map<String, LinkedList<Long>> endpointRemoteOutHistory,
                  Map<String, LinkedList<Long>> endpointRemoteStubInHistory,
-                 Map<String, LinkedList<Long>> endpointRemoteStubOutHistory) {
+                 Map<String, LinkedList<Long>> endpointRemoteStubOutHistory,
+                 Map<String, LinkedList<Long>> endpointInSizeHistory,
+                 Map<String, LinkedList<Long>> endpointOutSizeHistory) {
         this.ctx = ctx;
         this.endpointInHistory = endpointInHistory;
         this.endpointOutHistory = endpointOutHistory;
@@ -77,6 +82,8 @@ class EndpointsTab implements MonitorTab {
         this.endpointRemoteOutHistory = endpointRemoteOutHistory;
         this.endpointRemoteStubInHistory = endpointRemoteStubInHistory;
         this.endpointRemoteStubOutHistory = endpointRemoteStubOutHistory;
+        this.endpointInSizeHistory = endpointInSizeHistory;
+        this.endpointOutSizeHistory = endpointOutSizeHistory;
     }
 
     @Override
@@ -131,6 +138,9 @@ class EndpointsTab implements MonitorTab {
         }
         sortedEndpoints.sort(this::sortEndpoint);
 
+        boolean hasSize = info.endpoints.stream()
+                .anyMatch(ep -> ep.meanBodySize >= 0 || ep.meanHeadersSize >= 
0);
+
         List<Row> rows = new ArrayList<>();
         for (EndpointInfo ep : sortedEndpoints) {
             String dir = ep.direction != null ? ep.direction : "";
@@ -145,45 +155,61 @@ class EndpointsTab implements MonitorTab {
                 default -> "↔ ";
             };
 
-            rows.add(Row.from(
-                    Cell.from(Span.styled(ep.component != null ? ep.component 
: "", Style.EMPTY.fg(Color.CYAN))),
-                    Cell.from(ep.routeId != null ? ep.routeId : ""),
-                    Cell.from(Span.styled(arrow + dir, dirStyle)),
-                    rightCell(ep.hits > 0 ? String.valueOf(ep.hits) : "", 8),
-                    centerCell(ep.stub ? "x" : "", 6),
-                    centerCell(ep.remote ? "x" : "", 8),
-                    Cell.from(ep.uri != null ? ep.uri : "")));
+            List<Cell> cells = new ArrayList<>();
+            cells.add(Cell.from(Span.styled(ep.component != null ? 
ep.component : "", Style.EMPTY.fg(Color.CYAN))));
+            cells.add(Cell.from(ep.routeId != null ? ep.routeId : ""));
+            cells.add(Cell.from(Span.styled(arrow + dir, dirStyle)));
+            cells.add(rightCell(ep.hits > 0 ? String.valueOf(ep.hits) : "", 
8));
+            if (hasSize) {
+                cells.add(rightCell(sizeToString(ep.meanBodySize), 10));
+                cells.add(rightCell(sizeToString(ep.meanHeadersSize), 10));
+            }
+            cells.add(centerCell(ep.stub ? "x" : "", 6));
+            cells.add(centerCell(ep.remote ? "x" : "", 8));
+            cells.add(Cell.from(ep.uri != null ? ep.uri : ""));
+            rows.add(Row.from(cells));
         }
 
+        int emptyCols = hasSize ? 9 : 7;
         if (rows.isEmpty()) {
-            rows.add(Row.from(
-                    Cell.from(Span.styled("No endpoints", Style.EMPTY.dim())),
-                    Cell.from(""),
-                    Cell.from(""),
-                    Cell.from(""),
-                    Cell.from(""),
-                    Cell.from(""),
-                    Cell.from("")));
+            List<Cell> emptyCells = new ArrayList<>();
+            emptyCells.add(Cell.from(Span.styled("No endpoints", 
Style.EMPTY.dim())));
+            for (int i = 1; i < emptyCols; i++) {
+                emptyCells.add(Cell.from(""));
+            }
+            rows.add(Row.from(emptyCells));
+        }
+
+        List<Cell> headerCells = new ArrayList<>();
+        headerCells.add(Cell.from(Span.styled(sortLabel("COMPONENT", 
"component"), sortStyle("component"))));
+        headerCells.add(Cell.from(Span.styled(sortLabel("ROUTE", "route"), 
sortStyle("route"))));
+        headerCells.add(Cell.from(Span.styled(sortLabel("DIR", "dir"), 
sortStyle("dir"))));
+        headerCells.add(rightCell(sortLabel("TOTAL", "total"), 8, 
sortStyle("total")));
+        if (hasSize) {
+            headerCells.add(rightCell(sortLabel("BODY", "body"), 10, 
sortStyle("body")));
+            headerCells.add(rightCell(sortLabel("HDR", "hdr"), 10, 
sortStyle("hdr")));
+        }
+        headerCells.add(centerCell("STUB", 6, Style.EMPTY.bold()));
+        headerCells.add(centerCell("REMOTE", 8, Style.EMPTY.bold()));
+        headerCells.add(Cell.from(Span.styled(sortLabel("URI", "uri"), 
sortStyle("uri"))));
+
+        List<Constraint> widths = new ArrayList<>();
+        widths.add(Constraint.length(15));
+        widths.add(Constraint.length(20));
+        widths.add(Constraint.length(8));
+        widths.add(Constraint.length(8));
+        if (hasSize) {
+            widths.add(Constraint.length(10));
+            widths.add(Constraint.length(10));
         }
+        widths.add(Constraint.length(6));
+        widths.add(Constraint.length(8));
+        widths.add(Constraint.fill());
 
         Table table = Table.builder()
                 .rows(rows)
-                .header(Row.from(
-                        Cell.from(Span.styled(sortLabel("COMPONENT", 
"component"), sortStyle("component"))),
-                        Cell.from(Span.styled(sortLabel("ROUTE", "route"), 
sortStyle("route"))),
-                        Cell.from(Span.styled(sortLabel("DIR", "dir"), 
sortStyle("dir"))),
-                        rightCell(sortLabel("TOTAL", "total"), 8, 
sortStyle("total")),
-                        centerCell("STUB", 6, Style.EMPTY.bold()),
-                        centerCell("REMOTE", 8, Style.EMPTY.bold()),
-                        Cell.from(Span.styled(sortLabel("URI", "uri"), 
sortStyle("uri")))))
-                .widths(
-                        Constraint.length(15),
-                        Constraint.length(20),
-                        Constraint.length(8),
-                        Constraint.length(8),
-                        Constraint.length(6),
-                        Constraint.length(8),
-                        Constraint.fill())
+                .header(Row.from(headerCells))
+                .widths(widths.toArray(Constraint[]::new))
                 .block(Block.builder().borderType(BorderType.ROUNDED)
                         .title(" Endpoints sort:" + sort
                                + (filter == 1 ? " filter:remote" : filter == 2 
? " filter:remote+stub" : "")
@@ -191,6 +217,9 @@ class EndpointsTab implements MonitorTab {
                         .build())
                 .build();
 
+        boolean hasSizeHistory = !endpointInSizeHistory.isEmpty()
+                && endpointInSizeHistory.values().stream().anyMatch(h -> 
h.stream().anyMatch(v -> v > 0));
+
         List<Rect> chunks = showChart
                 ? Layout.vertical().constraints(Constraint.fill(), 
Constraint.length(16)).split(area)
                 : List.of(area);
@@ -206,7 +235,16 @@ class EndpointsTab implements MonitorTab {
                     .filter(ep -> "out".equals(ep.direction) && 
matchesFilter(ep))
                     .mapToLong(ep -> ep.hits)
                     .sum();
-            renderEndpointFlow(frame, chunks.get(1), inTotal, outTotal, 
info.name, info.pid);
+
+            if (hasSizeHistory) {
+                List<Rect> chartSplit = Layout.horizontal()
+                        .constraints(Constraint.percentage(50), 
Constraint.percentage(50))
+                        .split(chunks.get(1));
+                renderEndpointFlow(frame, chartSplit.get(0), inTotal, 
outTotal, info.name, info.pid);
+                renderPayloadSizeChart(frame, chartSplit.get(1), info.pid);
+            } else {
+                renderEndpointFlow(frame, chunks.get(1), inTotal, outTotal, 
info.name, info.pid);
+            }
         }
     }
 
@@ -253,6 +291,8 @@ class EndpointsTab implements MonitorTab {
                 yield da.compareToIgnoreCase(db);
             }
             case "total" -> Long.compare(b.hits, a.hits);
+            case "body" -> Long.compare(b.meanBodySize, a.meanBodySize);
+            case "hdr" -> Long.compare(b.meanHeadersSize, a.meanHeadersSize);
             case "uri" -> {
                 String ua = a.uri != null ? a.uri : "";
                 String ub = b.uri != null ? b.uri : "";
@@ -373,6 +413,76 @@ class EndpointsTab implements MonitorTab {
                 .build(), rightArea);
     }
 
+    private void renderPayloadSizeChart(Frame frame, Rect area, String pid) {
+        LinkedList<Long> inHist = endpointInSizeHistory.getOrDefault(pid, new 
LinkedList<>());
+        LinkedList<Long> outHist = endpointOutSizeHistory.getOrDefault(pid, 
new LinkedList<>());
+
+        int renderPoints = MAX_CHART_POINTS;
+        long[] inArr = new long[renderPoints];
+        long[] outArr = new long[renderPoints];
+        for (int i = 0; i < renderPoints; i++) {
+            int idx = inHist.size() - renderPoints + i;
+            if (idx >= 0) {
+                inArr[i] = inHist.get(idx);
+            }
+            idx = outHist.size() - renderPoints + i;
+            if (idx >= 0) {
+                outArr[i] = outHist.get(idx);
+            }
+        }
+        long curIn = inArr[renderPoints - 1];
+        long curOut = outArr[renderPoints - 1];
+
+        Line chartTitle = Line.from(
+                Span.styled("â–¬", Style.EMPTY.fg(Color.YELLOW)),
+                Span.raw(String.format(" in:%-8s ", sizeToString(curIn))),
+                Span.styled("â–¬", Style.EMPTY.fg(Color.MAGENTA)),
+                Span.raw(String.format(" out:%-8s avg body", 
sizeToString(curOut))));
+
+        frame.renderWidget(MirroredSparkline.builder()
+                .topData(inArr)
+                .bottomData(outArr)
+                .topStyle(Style.EMPTY.fg(Color.YELLOW))
+                .bottomStyle(Style.EMPTY.fg(Color.MAGENTA))
+                .yLabelFormatter(EndpointsTab::sizeToYLabel)
+                .xLabels("-" + renderPoints + "s", "-" + (renderPoints * 3 / 
4) + "s",
+                        "-" + (renderPoints / 2) + "s", "-" + (renderPoints / 
4) + "s", "now")
+                .block(Block.builder().borderType(BorderType.ROUNDED)
+                        .title(Title.from(chartTitle)).build())
+                .build(), area);
+    }
+
+    private static String sizeToYLabel(long size) {
+        if (size <= 0) {
+            return "0 B";
+        }
+        if (size < 1024) {
+            return size + "B";
+        } else if (size < 1024 * 1024) {
+            long kb = size / 1024;
+            return kb + "KB";
+        } else {
+            long mb = size / (1024 * 1024);
+            return mb + "MB";
+        }
+    }
+
+    static String sizeToString(long size) {
+        if (size < 0) {
+            return "-";
+        }
+        if (size == 0) {
+            return "0 B";
+        }
+        if (size < 1024) {
+            return size + " B";
+        } else if (size < 1024 * 1024) {
+            return String.format(Locale.US, "%.1f KB", size / 1024.0);
+        } else {
+            return String.format(Locale.US, "%.1f MB", size / (1024.0 * 
1024.0));
+        }
+    }
+
     @Override
     public SelectionContext getSelectionContext() {
         IntegrationInfo info = ctx.findSelectedIntegration();
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MirroredSparkline.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MirroredSparkline.java
index 5f60132fd390..30b8ad52e573 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MirroredSparkline.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MirroredSparkline.java
@@ -17,6 +17,7 @@
 package org.apache.camel.dsl.jbang.core.commands.tui;
 
 import java.util.List;
+import java.util.function.LongFunction;
 
 import dev.tamboui.buffer.Buffer;
 import dev.tamboui.layout.Rect;
@@ -107,6 +108,7 @@ public final class MirroredSparkline implements Widget {
     private final Sparkline.BarSet barSet;
     private final boolean showYAxis;
     private final String[] xLabels;
+    private final LongFunction<String> yLabelFormatter;
 
     private MirroredSparkline(Builder builder) {
         this.topData = builder.topData;
@@ -118,6 +120,7 @@ public final class MirroredSparkline implements Widget {
         this.barSet = builder.barSet;
         this.showYAxis = builder.showYAxis;
         this.xLabels = builder.xLabels;
+        this.yLabelFormatter = builder.yLabelFormatter;
     }
 
     /**
@@ -168,12 +171,10 @@ public final class MirroredSparkline implements Widget {
 
             if (showYAxis) {
                 String label;
-                if (r == 0) {
-                    label = effectiveMax > 9999 ? "999+" : 
String.format("%4d", effectiveMax);
+                if (r == 0 || r == chartBodyRows - 1) {
+                    label = formatYLabel(effectiveMax);
                 } else if (r == centerRow) {
                     label = "   0";
-                } else if (r == chartBodyRows - 1) {
-                    label = effectiveMax > 9999 ? "999+" : 
String.format("%4d", effectiveMax);
                 } else {
                     label = "    ";
                 }
@@ -251,6 +252,17 @@ public final class MirroredSparkline implements Widget {
         }
     }
 
+    private String formatYLabel(long value) {
+        if (yLabelFormatter != null) {
+            String s = yLabelFormatter.apply(value);
+            if (s.length() >= Y_LABEL_WIDTH) {
+                return s.substring(0, Y_LABEL_WIDTH);
+            }
+            return " ".repeat(Y_LABEL_WIDTH - s.length()) + s;
+        }
+        return value > 9999 ? "999+" : String.format("%4d", value);
+    }
+
     private long computeMax() {
         if (max != null) {
             return Math.max(1, max);
@@ -278,6 +290,7 @@ public final class MirroredSparkline implements Widget {
         private Sparkline.BarSet barSet = Sparkline.BarSet.NINE_LEVELS;
         private boolean showYAxis = true;
         private String[] xLabels;
+        private LongFunction<String> yLabelFormatter;
 
         private Builder() {
         }
@@ -418,6 +431,19 @@ public final class MirroredSparkline implements Widget {
             return this;
         }
 
+        /**
+         * Sets a custom formatter for Y-axis max labels. The function 
receives the max value and should return a short
+         * string (up to 4 chars). When not set, values are formatted as 
integers with {@code 999+} for values above
+         * 9999.
+         *
+         * @param  formatter the formatter function
+         * @return           this builder
+         */
+        public Builder yLabelFormatter(LongFunction<String> formatter) {
+            this.yLabelFormatter = formatter;
+            return this;
+        }
+
         /**
          * Builds the widget.
          *

Reply via email to