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 d0eba277e089 CAMEL-24259: camel-jbang - TUI consumer tab extract 
quartz cron schedule
d0eba277e089 is described below

commit d0eba277e08979940d0d56895d2d3d509aa148f6
Author: Claus Ibsen <[email protected]>
AuthorDate: Sun Jul 26 21:00:01 2026 +0200

    CAMEL-24259: camel-jbang - TUI consumer tab extract quartz cron schedule
    
    CAMEL-24259: camel-jbang - TUI add Network Services tab
    
    Add a Network Services tab that surfaces EndpointServiceRegistry data
    showing network-facing endpoints (HTTP, Kafka, database, etc.) with
    direction, protocol, hosted flag, and hit counts. Extract FlowHelper
    to share flow diagram and sparkline chart rendering between
    EndpointsTab and NetworkTab.
    
    CAMEL-24259: camel-jbang - TUI payload size visibility for Network and 
Endpoints tabs
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../camel/component/quartz/QuartzConsole.java      |  35 ++
 .../impl/engine/DefaultMessageSizeStrategy.java    |  42 ++
 .../engine/DefaultRuntimeEndpointRegistry.java     |  35 +-
 .../dsl/jbang/core/commands/tui/ConsumerInfo.java  |   1 +
 .../dsl/jbang/core/commands/tui/ConsumersTab.java  |   4 +
 .../core/commands/tui/DataRefreshService.java      |   1 +
 .../dsl/jbang/core/commands/tui/EndpointsTab.java  | 265 ++---------
 .../dsl/jbang/core/commands/tui/FlowHelper.java    | 205 ++++++++
 .../jbang/core/commands/tui/IntegrationInfo.java   |   1 +
 .../jbang/core/commands/tui/MetricsCollector.java  | 117 +++++
 .../dsl/jbang/core/commands/tui/NetworkTab.java    | 516 +++++++++++++++++++++
 .../tui/{ConsumerInfo.java => ServiceInfo.java}    |  23 +-
 .../dsl/jbang/core/commands/tui/StatusParser.java  |  49 ++
 .../dsl/jbang/core/commands/tui/TabRegistry.java   |   3 +
 .../dsl/jbang/core/commands/tui/TuiIcons.java      |   1 +
 .../core/commands/tui/ConsumersTabRenderTest.java  |  21 +
 .../jbang/core/commands/tui/TabRegistryTest.java   |   4 +-
 17 files changed, 1071 insertions(+), 252 deletions(-)

diff --git 
a/components/camel-quartz/src/main/java/org/apache/camel/component/quartz/QuartzConsole.java
 
b/components/camel-quartz/src/main/java/org/apache/camel/component/quartz/QuartzConsole.java
index 04f677dca925..62079cba0ac0 100644
--- 
a/components/camel-quartz/src/main/java/org/apache/camel/component/quartz/QuartzConsole.java
+++ 
b/components/camel-quartz/src/main/java/org/apache/camel/component/quartz/QuartzConsole.java
@@ -20,14 +20,18 @@ import java.text.SimpleDateFormat;
 import java.util.Date;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 import org.apache.camel.spi.annotations.DevConsole;
 import org.apache.camel.support.console.AbstractDevConsole;
 import org.apache.camel.util.json.JsonArray;
 import org.apache.camel.util.json.JsonObject;
+import org.quartz.JobDetail;
 import org.quartz.JobExecutionContext;
+import org.quartz.JobKey;
 import org.quartz.Scheduler;
 import org.quartz.SchedulerMetaData;
+import org.quartz.impl.matchers.GroupMatcher;
 
 @DevConsole(name = "quartz", description = "Quartz Scheduler")
 public class QuartzConsole extends AbstractDevConsole {
@@ -192,6 +196,37 @@ public class QuartzConsole extends AbstractDevConsole {
                         arr.add(jo);
                     }
                 }
+
+                // all scheduled triggers (for TUI consumer schedule display)
+                Set<JobKey> jobKeys = 
scheduler.getJobKeys(GroupMatcher.anyGroup());
+                if (!jobKeys.isEmpty()) {
+                    JsonArray arr = new JsonArray();
+                    root.put("triggers", arr);
+                    for (JobKey jobKey : jobKeys) {
+                        JobDetail job = scheduler.getJobDetail(jobKey);
+                        if (job != null) {
+                            JsonObject jo = new JsonObject();
+                            String routeId = (String) 
job.getJobDataMap().get("routeId");
+                            if (routeId != null) {
+                                jo.put("routeId", routeId);
+                            }
+                            String type = (String) 
job.getJobDataMap().get(QuartzConstants.QUARTZ_TRIGGER_TYPE);
+                            if (type != null) {
+                                jo.put("triggerType", type);
+                            }
+                            String cron = (String) 
job.getJobDataMap().get(QuartzConstants.QUARTZ_TRIGGER_CRON_EXPRESSION);
+                            if (cron != null) {
+                                jo.put("cron", cron);
+                            }
+                            String interval
+                                    = (String) 
job.getJobDataMap().get(QuartzConstants.QUARTZ_TRIGGER_SIMPLE_REPEAT_INTERVAL);
+                            if (interval != null) {
+                                jo.put("repeatInterval", interval);
+                            }
+                            arr.add(jo);
+                        }
+                    }
+                }
             } catch (Exception e) {
                 // ignore
             }
diff --git 
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultMessageSizeStrategy.java
 
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultMessageSizeStrategy.java
index e909a29022b1..94e10db706ca 100644
--- 
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultMessageSizeStrategy.java
+++ 
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultMessageSizeStrategy.java
@@ -20,6 +20,7 @@ import java.io.File;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.util.Collection;
 import java.util.Map;
 
 import org.apache.camel.CamelContext;
@@ -83,6 +84,12 @@ public class DefaultMessageSizeStrategy extends 
ServiceSupport implements CamelC
             if (body instanceof Path p) {
                 return Files.size(p);
             }
+            if (body instanceof Collection<?> col) {
+                return estimateCollectionSize(col);
+            }
+            if (body instanceof Map<?, ?> map) {
+                return estimateMapSize(map);
+            }
             // fallback to Content-Length header (e.g. HTTP components where 
body is a stream)
             Long cl = message.getHeader(Exchange.CONTENT_LENGTH, Long.class);
             if (cl != null && cl >= 0) {
@@ -122,4 +129,39 @@ public class DefaultMessageSizeStrategy extends 
ServiceSupport implements CamelC
         }
         return -1;
     }
+
+    private long estimateCollectionSize(Collection<?> col) {
+        long total = 0;
+        for (Object item : col) {
+            if (item instanceof Map<?, ?> map) {
+                total += estimateMapSize(map);
+            } else if (item instanceof String str) {
+                total += str.getBytes(StandardCharsets.UTF_8).length;
+            } else if (item instanceof byte[] bytes) {
+                total += bytes.length;
+            } else if (item != null) {
+                total += 
item.toString().getBytes(StandardCharsets.UTF_8).length;
+            }
+        }
+        return total;
+    }
+
+    private long estimateMapSize(Map<?, ?> map) {
+        long total = 0;
+        for (Map.Entry<?, ?> entry : map.entrySet()) {
+            Object key = entry.getKey();
+            if (key != null) {
+                total += 
key.toString().getBytes(StandardCharsets.UTF_8).length;
+            }
+            Object value = entry.getValue();
+            if (value instanceof String str) {
+                total += str.getBytes(StandardCharsets.UTF_8).length;
+            } else if (value instanceof byte[] bytes) {
+                total += bytes.length;
+            } else if (value != null) {
+                total += 
value.toString().getBytes(StandardCharsets.UTF_8).length;
+            }
+        }
+        return total;
+    }
 }
diff --git 
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultRuntimeEndpointRegistry.java
 
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultRuntimeEndpointRegistry.java
index 8f77ff2ed4de..0fc53cfee529 100644
--- 
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultRuntimeEndpointRegistry.java
+++ 
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultRuntimeEndpointRegistry.java
@@ -34,6 +34,7 @@ import org.apache.camel.spi.CamelEvent.ExchangeCompletedEvent;
 import org.apache.camel.spi.CamelEvent.ExchangeCreatedEvent;
 import org.apache.camel.spi.CamelEvent.ExchangeFailedEvent;
 import org.apache.camel.spi.CamelEvent.ExchangeSendingEvent;
+import org.apache.camel.spi.CamelEvent.ExchangeSentEvent;
 import org.apache.camel.spi.CamelEvent.RouteAddedEvent;
 import org.apache.camel.spi.CamelEvent.RouteRemovedEvent;
 import org.apache.camel.spi.EndpointUtilizationStatistics;
@@ -324,11 +325,16 @@ public class DefaultRuntimeEndpointRegistry extends 
EventNotifierSupport impleme
                 if (key != null) {
                     outputUtilization.onHit(key);
                     if (messageSizeEnabled) {
+                        // Record the request body size being sent to the 
endpoint.
+                        // ExchangeSentEvent will also fire after the producer 
completes
+                        // and record the max of request vs response body size.
                         Message message = exchange.getIn();
                         MessageSizeStrategy strategy = 
getCamelContext().getMessageSizeStrategy();
                         long bodySize = strategy.computeBodySize(message);
                         long headersSize = 
strategy.computeHeadersSize(message);
-                        outputSizeStats.onHit(key, bodySize, headersSize);
+                        // Store the request body size so ExchangeSentEvent 
can compare
+                        exchange.setProperty("CamelMessageBodySizeSending", 
bodySize);
+                        exchange.setProperty("CamelMessageHeadersSizeSending", 
headersSize);
                         // reset stream cache so the body is re-readable when 
sending
                         if (message.getBody() instanceof StreamCache sc) {
                             sc.reset();
@@ -336,6 +342,32 @@ public class DefaultRuntimeEndpointRegistry extends 
EventNotifierSupport impleme
                     }
                 }
             }
+        } else if (extended && messageSizeEnabled && event instanceof 
ExchangeSentEvent ese) {
+            // Record the max of request and response body size.
+            // For SQL SELECT the response (List<Map>) is the meaningful 
payload.
+            // For SQL INSERT the request (the data being inserted) is the 
meaningful payload.
+            // Taking the max ensures the most relevant size is reported in 
both cases.
+            Endpoint endpoint = ese.getEndpoint();
+            Exchange exchange = ese.getExchange();
+            String routeId = ExchangeHelper.getRouteId(exchange);
+            String uri = endpoint.getEndpointUri();
+            String key = asUtilizationKey(routeId, uri);
+            if (key != null) {
+                MessageSizeStrategy strategy = 
getCamelContext().getMessageSizeStrategy();
+                long responseBodySize = 
strategy.computeBodySize(exchange.getMessage());
+                long responseHeadersSize = 
strategy.computeHeadersSize(exchange.getMessage());
+                if (exchange.getMessage().getBody() instanceof StreamCache sc) 
{
+                    sc.reset();
+                }
+                Long requestBodySize = 
exchange.getProperty("CamelMessageBodySizeSending", Long.class);
+                Long requestHeadersSize = 
exchange.getProperty("CamelMessageHeadersSizeSending", Long.class);
+                long bodySize = Math.max(responseBodySize, requestBodySize != 
null ? requestBodySize : 0);
+                long headersSize = Math.max(responseHeadersSize, 
requestHeadersSize != null ? requestHeadersSize : 0);
+                outputSizeStats.onHit(key, bodySize, headersSize);
+                // cleanup temporary properties
+                exchange.removeProperty("CamelMessageBodySizeSending");
+                exchange.removeProperty("CamelMessageHeadersSizeSending");
+            }
         } else if (event instanceof ExchangeCompletedEvent || event instanceof 
ExchangeFailedEvent) {
             // InOut consumers send a reply back when the exchange completes;
             // record this as an "out" hit on the consumer's fromEndpoint
@@ -381,6 +413,7 @@ public class DefaultRuntimeEndpointRegistry extends 
EventNotifierSupport impleme
     public boolean isEnabled(CamelEvent event) {
         return enabled && event instanceof ExchangeCreatedEvent
                 || event instanceof ExchangeSendingEvent
+                || event instanceof ExchangeSentEvent
                 || event instanceof ExchangeCompletedEvent
                 || event instanceof ExchangeFailedEvent
                 || event instanceof RouteAddedEvent
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumerInfo.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumerInfo.java
index e9e1ad6f959b..17ce5aa06c36 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumerInfo.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumerInfo.java
@@ -27,6 +27,7 @@ class ConsumerInfo {
     Long totalCounter;
     Long delay;
     Long period;
+    String schedule;
     String sinceLastStarted;
     String sinceLastCompleted;
     String sinceLastFailed;
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 c3152dac3e23..74bd30692436 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
@@ -207,6 +207,10 @@ class ConsumersTab extends AbstractTableTab {
     }
 
     static String consumerSchedule(ConsumerInfo ci) {
+        // Prefer server-provided schedule (e.g., from quartz dev console)
+        if (ci.schedule != null) {
+            return ci.schedule;
+        }
         // Try to extract cron expression from URI for cron/quartz components
         String cron = extractCronFromUri(ci.uri);
         if (cron != null) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DataRefreshService.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DataRefreshService.java
index bb635f6b27ad..efe068e5d8c5 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DataRefreshService.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DataRefreshService.java
@@ -264,6 +264,7 @@ class DataRefreshService {
                     infos.add(info);
                     metrics.updateThroughputHistory(info);
                     metrics.updateEndpointHistory(info);
+                    metrics.updateServiceHistory(info);
                     metrics.updateCbHistory(info);
                     metrics.updateHeapHistory(info);
                     metrics.updateLoadMetrics(ph, info);
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 089af5847cc2..e68a0b6d292b 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
@@ -23,7 +23,6 @@ import java.util.HashSet;
 import java.util.LinkedHashMap;
 import java.util.LinkedList;
 import java.util.List;
-import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
 
@@ -34,7 +33,6 @@ import dev.tamboui.markdown.MarkdownView;
 import dev.tamboui.style.Style;
 import dev.tamboui.terminal.Frame;
 import dev.tamboui.text.CharWidth;
-import dev.tamboui.text.Line;
 import dev.tamboui.text.Span;
 import dev.tamboui.tui.event.KeyCode;
 import dev.tamboui.tui.event.KeyEvent;
@@ -43,9 +41,6 @@ import dev.tamboui.tui.event.MouseEventKind;
 import dev.tamboui.widgets.block.Block;
 import dev.tamboui.widgets.block.BorderType;
 import dev.tamboui.widgets.block.Borders;
-import dev.tamboui.widgets.block.Title;
-import dev.tamboui.widgets.paragraph.Paragraph;
-import dev.tamboui.widgets.sparkline.DualSparkline;
 import dev.tamboui.widgets.table.Cell;
 import dev.tamboui.widgets.table.Row;
 import dev.tamboui.widgets.table.Table;
@@ -78,6 +73,8 @@ class EndpointsTab extends AbstractTableTab {
     private final Map<String, LinkedList<Long>> endpointOutSizeHistory;
     private final Map<String, LinkedList<Long>> perEndpointInHistory;
     private final Map<String, LinkedList<Long>> perEndpointOutHistory;
+    private final Map<String, LinkedList<Long>> perEndpointInSizeHistory;
+    private final Map<String, LinkedList<Long>> perEndpointOutSizeHistory;
 
     private int filter;
     private int chartMode = CHART_ALL;
@@ -106,6 +103,8 @@ class EndpointsTab extends AbstractTableTab {
         this.endpointOutSizeHistory = metrics.getEndpointOutSizeHistory();
         this.perEndpointInHistory = metrics.getPerEndpointInHistory();
         this.perEndpointOutHistory = metrics.getPerEndpointOutHistory();
+        this.perEndpointInSizeHistory = metrics.getPerEndpointInSizeHistory();
+        this.perEndpointOutSizeHistory = 
metrics.getPerEndpointOutSizeHistory();
     }
 
     @Override
@@ -236,8 +235,8 @@ class EndpointsTab extends AbstractTableTab {
             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(rightCell(FlowHelper.sizeToString(ep.meanBodySize), 
10));
+                
cells.add(rightCell(FlowHelper.sizeToString(ep.meanHeadersSize), 10));
             }
             cells.add(centerCell(ep.stub ? "x" : "", 6));
             cells.add(centerCell(ep.remote ? "x" : "", 8));
@@ -291,8 +290,9 @@ class EndpointsTab extends AbstractTableTab {
 
         boolean hasSizeHistory;
         try {
-            hasSizeHistory = !endpointInSizeHistory.isEmpty()
-                    && endpointInSizeHistory.values().stream()
+            hasSizeHistory = endpointInSizeHistory.values().stream()
+                    .anyMatch(h -> new ArrayList<>(h).stream().anyMatch(v -> v 
> 0))
+                    || endpointOutSizeHistory.values().stream()
                             .anyMatch(h -> new 
ArrayList<>(h).stream().anyMatch(v -> v > 0));
         } catch (java.util.ConcurrentModificationException e) {
             hasSizeHistory = false;
@@ -442,57 +442,7 @@ class EndpointsTab extends AbstractTableTab {
                 .split(area);
         hSplit.setBorderPos(hParts.get(1).x());
 
-        int w = Math.max(10, hParts.get(0).width() - 2);
-
-        String label = name != null ? name : "INTEGRATION";
-        if (CharWidth.of(label) > 20) {
-            label = CharWidth.truncateWithEllipsis(label, 20, 
CharWidth.TruncatePosition.END);
-        }
-        String box = "[ " + label + " ]";
-        int boxLen = CharWidth.of(box);
-
-        int sideLen = Math.max(4, (w - boxLen - 2) / 2);
-        String arm = "─".repeat(Math.max(1, sideLen - 1));
-        String arrowStr = arm + TuiIcons.POINTER;
-
-        String inStr = String.valueOf(inTotal);
-        String outStr = String.valueOf(outTotal);
-
-        int inPad = Math.max(0, sideLen - inStr.length());
-        int centerGap = boxLen + 2;
-        int outPad = Math.max(0, sideLen - outStr.length());
-
-        int inLabelPad = (sideLen - 2) / 2;
-        int outLabelPad = (sideLen - 3) / 2;
-        String inLabelStr = " ".repeat(inLabelPad) + "in" + " ".repeat(sideLen 
- inLabelPad - 2);
-        String outLabelStr = " ".repeat(outLabelPad) + "out";
-
-        Style inStyle = Theme.success();
-        Style outStyle = Style.EMPTY.fg(Theme.accent());
-        Style dimStyle = Style.EMPTY.dim();
-
-        List<Line> flowLines = new ArrayList<>();
-        flowLines.add(Line.from(Span.raw("")));
-        flowLines.add(Line.from(Span.raw("")));
-        flowLines.add(Line.from(
-                Span.styled(" ".repeat(inPad) + inStr, inTotal > 0 ? inStyle : 
dimStyle),
-                Span.raw(" ".repeat(centerGap)),
-                Span.styled(outStr + " ".repeat(outPad), outTotal > 0 ? 
outStyle : dimStyle)));
-        flowLines.add(Line.from(
-                Span.styled(arrowStr, inStyle),
-                Span.raw(" "),
-                Span.styled(box, Theme.label().bold()),
-                Span.raw(" "),
-                Span.styled(arrowStr, outStyle)));
-        flowLines.add(Line.from(
-                Span.styled(inLabelStr, inStyle.dim()),
-                Span.raw(" ".repeat(centerGap)),
-                Span.styled(outLabelStr, outStyle.dim())));
-
-        frame.renderWidget(Paragraph.builder()
-                .text(dev.tamboui.text.Text.from(flowLines))
-                
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL).title("
 Flow ").build())
-                .build(), hParts.get(0));
+        FlowHelper.renderFlowPanel(frame, hParts.get(0), inTotal, outTotal, 
name);
 
         Map<String, LinkedList<Long>> inHistMap = switch (filter) {
             case 1 -> endpointRemoteInHistory;
@@ -507,40 +457,7 @@ class EndpointsTab extends AbstractTableTab {
         LinkedList<Long> inHist = inHistMap.getOrDefault(pid, new 
LinkedList<>());
         LinkedList<Long> outHist = outHistMap.getOrDefault(pid, new 
LinkedList<>());
 
-        int renderPoints = Math.max(20, (Math.min(MAX_CHART_POINTS, 
hParts.get(1).width() - 6) / 20) * 20);
-        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] = unbox(inHist.get(idx));
-            }
-            idx = outHist.size() - renderPoints + i;
-            if (idx >= 0) {
-                outArr[i] = unbox(outHist.get(idx));
-            }
-        }
-        long curIn = inArr[renderPoints - 1];
-        long curOut = outArr[renderPoints - 1];
-
-        Line chartTitle = Line.from(
-                Span.styled("▬", Theme.success()),
-                Span.raw(String.format(" in:%-4s ", 
MetricsCollector.formatThroughput(curIn))),
-                Span.styled("▬", Style.EMPTY.fg(Theme.accent())),
-                Span.raw(String.format(" out:%-4s msg/s ", 
MetricsCollector.formatThroughput(curOut))));
-
-        Rect rightArea = hParts.get(1);
-        frame.renderWidget(DualSparkline.builder()
-                .topData(inArr)
-                .bottomData(outArr)
-                .topStyle(Theme.success())
-                .bottomStyle(Style.EMPTY.fg(Theme.accent()))
-                .showYAxis(true)
-                .xLabels("-" + renderPoints + "s", "-" + (renderPoints * 3 / 
4) + "s",
-                        "-" + (renderPoints / 2) + "s", "-" + (renderPoints / 
4) + "s", "now")
-                
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
-                        .title(Title.from(chartTitle)).build())
-                .build(), rightArea);
+        FlowHelper.renderThroughputChart(frame, hParts.get(1), inHist, 
outHist);
     }
 
     private void renderSingleEndpointChart(Frame frame, Rect area, String 
selectedUri, IntegrationInfo info) {
@@ -559,139 +476,37 @@ class EndpointsTab extends AbstractTableTab {
                 .split(area);
         hSplit.setBorderPos(hParts.get(1).x());
 
-        // Flow diagram with endpoint URI as label
-        int w = Math.max(10, hParts.get(0).width() - 2);
-        String label = selectedUri;
-        if (CharWidth.of(label) > 20) {
-            label = CharWidth.truncateWithEllipsis(label, 20, 
CharWidth.TruncatePosition.END);
-        }
-        String box = "[ " + label + " ]";
-        int boxLen = CharWidth.of(box);
-        int sideLen = Math.max(4, (w - boxLen - 2) / 2);
-        String arm = "─".repeat(Math.max(1, sideLen - 1));
-        String arrowStr = arm + TuiIcons.POINTER;
-        String inStr = String.valueOf(inTotal);
-        String outStr = String.valueOf(outTotal);
-        int inPad = Math.max(0, sideLen - inStr.length());
-        int centerGap = boxLen + 2;
-        int outPad = Math.max(0, sideLen - outStr.length());
-        int inLabelPad = (sideLen - 2) / 2;
-        int outLabelPad = (sideLen - 3) / 2;
-        String inLabelStr = " ".repeat(inLabelPad) + "in" + " ".repeat(sideLen 
- inLabelPad - 2);
-        String outLabelStr = " ".repeat(outLabelPad) + "out";
-
-        Style inStyle = Theme.success();
-        Style outStyle = Style.EMPTY.fg(Theme.accent());
-        Style dimStyle = Style.EMPTY.dim();
-
-        List<Line> flowLines = new ArrayList<>();
-        flowLines.add(Line.from(Span.raw("")));
-        flowLines.add(Line.from(Span.raw("")));
-        flowLines.add(Line.from(
-                Span.styled(" ".repeat(inPad) + inStr, inTotal > 0 ? inStyle : 
dimStyle),
-                Span.raw(" ".repeat(centerGap)),
-                Span.styled(outStr + " ".repeat(outPad), outTotal > 0 ? 
outStyle : dimStyle)));
-        flowLines.add(Line.from(
-                Span.styled(arrowStr, inStyle),
-                Span.raw(" "),
-                Span.styled(box, Theme.label().bold()),
-                Span.raw(" "),
-                Span.styled(arrowStr, outStyle)));
-        flowLines.add(Line.from(
-                Span.styled(inLabelStr, inStyle.dim()),
-                Span.raw(" ".repeat(centerGap)),
-                Span.styled(outLabelStr, outStyle.dim())));
-
-        frame.renderWidget(Paragraph.builder()
-                .text(dev.tamboui.text.Text.from(flowLines))
-                
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL).title("
 Flow ").build())
-                .build(), hParts.get(0));
-
-        // Per-endpoint sparkline
+        FlowHelper.renderFlowPanel(frame, hParts.get(0), inTotal, outTotal, 
selectedUri);
+
         String key = info.pid + "|" + selectedUri;
         LinkedList<Long> inHist = perEndpointInHistory.getOrDefault(key, new 
LinkedList<>());
         LinkedList<Long> outHist = perEndpointOutHistory.getOrDefault(key, new 
LinkedList<>());
 
-        int renderPoints = Math.max(20, (Math.min(MAX_CHART_POINTS, 
hParts.get(1).width() - 6) / 20) * 20);
-        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] = unbox(inHist.get(idx));
-            }
-            idx = outHist.size() - renderPoints + i;
-            if (idx >= 0) {
-                outArr[i] = unbox(outHist.get(idx));
-            }
+        LinkedList<Long> inSizeHist = 
perEndpointInSizeHistory.getOrDefault(key, new LinkedList<>());
+        LinkedList<Long> outSizeHist = 
perEndpointOutSizeHistory.getOrDefault(key, new LinkedList<>());
+        boolean hasSizeData;
+        try {
+            hasSizeData = new ArrayList<>(inSizeHist).stream().anyMatch(v -> v 
> 0)
+                    || new ArrayList<>(outSizeHist).stream().anyMatch(v -> v > 
0);
+        } catch (java.util.ConcurrentModificationException e) {
+            hasSizeData = false;
         }
-        long curIn = inArr[renderPoints - 1];
-        long curOut = outArr[renderPoints - 1];
 
-        String uriLabel = selectedUri;
-        if (CharWidth.of(uriLabel) > 30) {
-            uriLabel = CharWidth.truncateWithEllipsis(uriLabel, 30, 
CharWidth.TruncatePosition.END);
+        if (hasSizeData) {
+            List<Rect> chartSplit = Layout.horizontal()
+                    .constraints(Constraint.percentage(50), 
Constraint.percentage(50))
+                    .split(hParts.get(1));
+            FlowHelper.renderThroughputChart(frame, chartSplit.get(0), inHist, 
outHist, selectedUri);
+            FlowHelper.renderPayloadSizeChart(frame, chartSplit.get(1), 
inSizeHist, outSizeHist);
+        } else {
+            FlowHelper.renderThroughputChart(frame, hParts.get(1), inHist, 
outHist, selectedUri);
         }
-
-        Line chartTitle = Line.from(
-                Span.raw(" ["),
-                Span.styled(uriLabel, Theme.label().bold()),
-                Span.raw("] "),
-                Span.styled("▬", Theme.success()),
-                Span.raw(String.format(" in:%-4s ", 
MetricsCollector.formatThroughput(curIn))),
-                Span.styled("▬", Style.EMPTY.fg(Theme.accent())),
-                Span.raw(String.format(" out:%-4s msg/s ", 
MetricsCollector.formatThroughput(curOut))));
-
-        frame.renderWidget(DualSparkline.builder()
-                .topData(inArr)
-                .bottomData(outArr)
-                .topStyle(Theme.success())
-                .bottomStyle(Style.EMPTY.fg(Theme.accent()))
-                .showYAxis(true)
-                .xLabels("-" + renderPoints + "s", "-" + (renderPoints * 3 / 
4) + "s",
-                        "-" + (renderPoints / 2) + "s", "-" + (renderPoints / 
4) + "s", "now")
-                
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
-                        .title(Title.from(chartTitle)).build())
-                .build(), hParts.get(1));
     }
 
     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 = Math.max(20, (Math.min(MAX_CHART_POINTS, 
area.width() - 6) / 20) * 20);
-        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] = unbox(inHist.get(idx));
-            }
-            idx = outHist.size() - renderPoints + i;
-            if (idx >= 0) {
-                outArr[i] = unbox(outHist.get(idx));
-            }
-        }
-        long curIn = inArr[renderPoints - 1];
-        long curOut = outArr[renderPoints - 1];
-
-        Line chartTitle = Line.from(
-                Span.styled("▬", Theme.label()),
-                Span.raw(String.format(" in:%-8s ", sizeToString(curIn))),
-                Span.styled("▬", Theme.notice()),
-                Span.raw(String.format(" out:%-8s avg body ", 
sizeToString(curOut))));
-
-        frame.renderWidget(DualSparkline.builder()
-                .topData(inArr)
-                .bottomData(outArr)
-                .topStyle(Theme.label())
-                .bottomStyle(Theme.notice())
-                .showYAxis(true)
-                .xLabels("-" + renderPoints + "s", "-" + (renderPoints * 3 / 
4) + "s",
-                        "-" + (renderPoints / 2) + "s", "-" + (renderPoints / 
4) + "s", "now")
-                
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
-                        .title(Title.from(chartTitle)).build())
-                .build(), area);
+        FlowHelper.renderPayloadSizeChart(frame, area, inHist, outHist);
     }
 
     private void renderDetail(Frame frame, Rect area, List<EndpointInfo> 
sortedEndpoints, IntegrationInfo info) {
@@ -848,26 +663,6 @@ class EndpointsTab extends AbstractTableTab {
         }
     }
 
-    private static long unbox(Long value) {
-        return value != null ? value : 0L;
-    }
-
-    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/FlowHelper.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FlowHelper.java
new file mode 100644
index 000000000000..3246c16ae52c
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FlowHelper.java
@@ -0,0 +1,205 @@
+/*
+ * 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.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Locale;
+
+import dev.tamboui.layout.Rect;
+import dev.tamboui.style.Style;
+import dev.tamboui.terminal.Frame;
+import dev.tamboui.text.CharWidth;
+import dev.tamboui.text.Line;
+import dev.tamboui.text.Span;
+import dev.tamboui.widgets.block.Block;
+import dev.tamboui.widgets.block.BorderType;
+import dev.tamboui.widgets.block.Borders;
+import dev.tamboui.widgets.block.Title;
+import dev.tamboui.widgets.paragraph.Paragraph;
+import dev.tamboui.widgets.sparkline.DualSparkline;
+
+final class FlowHelper {
+
+    static final int MAX_CHART_POINTS = 300;
+
+    private FlowHelper() {
+    }
+
+    static void renderFlowPanel(Frame frame, Rect area, long inTotal, long 
outTotal, String label) {
+        int w = Math.max(10, area.width() - 2);
+
+        String name = label != null ? label : "INTEGRATION";
+        if (CharWidth.of(name) > 20) {
+            name = CharWidth.truncateWithEllipsis(name, 20, 
CharWidth.TruncatePosition.END);
+        }
+        String box = "[ " + name + " ]";
+        int boxLen = CharWidth.of(box);
+
+        int sideLen = Math.max(4, (w - boxLen - 2) / 2);
+        String arm = "─".repeat(Math.max(1, sideLen - 1));
+        String arrowStr = arm + TuiIcons.POINTER;
+
+        String inStr = String.valueOf(inTotal);
+        String outStr = String.valueOf(outTotal);
+
+        int inPad = Math.max(0, sideLen - inStr.length());
+        int centerGap = boxLen + 2;
+        int outPad = Math.max(0, sideLen - outStr.length());
+
+        int inLabelPad = (sideLen - 2) / 2;
+        int outLabelPad = (sideLen - 3) / 2;
+        String inLabelStr = " ".repeat(inLabelPad) + "in" + " ".repeat(sideLen 
- inLabelPad - 2);
+        String outLabelStr = " ".repeat(outLabelPad) + "out";
+
+        Style inStyle = Theme.success();
+        Style outStyle = Style.EMPTY.fg(Theme.accent());
+        Style dimStyle = Style.EMPTY.dim();
+
+        List<Line> flowLines = new ArrayList<>();
+        flowLines.add(Line.from(Span.raw("")));
+        flowLines.add(Line.from(Span.raw("")));
+        flowLines.add(Line.from(
+                Span.styled(" ".repeat(inPad) + inStr, inTotal > 0 ? inStyle : 
dimStyle),
+                Span.raw(" ".repeat(centerGap)),
+                Span.styled(outStr + " ".repeat(outPad), outTotal > 0 ? 
outStyle : dimStyle)));
+        flowLines.add(Line.from(
+                Span.styled(arrowStr, inStyle),
+                Span.raw(" "),
+                Span.styled(box, Theme.label().bold()),
+                Span.raw(" "),
+                Span.styled(arrowStr, outStyle)));
+        flowLines.add(Line.from(
+                Span.styled(inLabelStr, inStyle.dim()),
+                Span.raw(" ".repeat(centerGap)),
+                Span.styled(outLabelStr, outStyle.dim())));
+
+        frame.renderWidget(Paragraph.builder()
+                .text(dev.tamboui.text.Text.from(flowLines))
+                
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL).title("
 Flow ").build())
+                .build(), area);
+    }
+
+    static void renderThroughputChart(
+            Frame frame, Rect area, LinkedList<Long> inHist, LinkedList<Long> 
outHist) {
+        renderThroughputChart(frame, area, inHist, outHist, null);
+    }
+
+    static void renderThroughputChart(
+            Frame frame, Rect area, LinkedList<Long> inHist, LinkedList<Long> 
outHist, String chartLabel) {
+        int renderPoints = Math.max(20, (Math.min(MAX_CHART_POINTS, 
area.width() - 6) / 20) * 20);
+        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] = unbox(inHist.get(idx));
+            }
+            idx = outHist.size() - renderPoints + i;
+            if (idx >= 0) {
+                outArr[i] = unbox(outHist.get(idx));
+            }
+        }
+        long curIn = inArr[renderPoints - 1];
+        long curOut = outArr[renderPoints - 1];
+
+        List<Span> titleSpans = new ArrayList<>();
+        if (chartLabel != null) {
+            String label = chartLabel;
+            if (CharWidth.of(label) > 30) {
+                label = CharWidth.truncateWithEllipsis(label, 30, 
CharWidth.TruncatePosition.END);
+            }
+            titleSpans.add(Span.raw(" ["));
+            titleSpans.add(Span.styled(label, Theme.label().bold()));
+            titleSpans.add(Span.raw("] "));
+        }
+        titleSpans.add(Span.styled("▬", Theme.success()));
+        titleSpans.add(Span.raw(String.format(" in:%-4s ", 
MetricsCollector.formatThroughput(curIn))));
+        titleSpans.add(Span.styled("▬", Style.EMPTY.fg(Theme.accent())));
+        titleSpans.add(Span.raw(String.format(" out:%-4s msg/s ", 
MetricsCollector.formatThroughput(curOut))));
+
+        frame.renderWidget(DualSparkline.builder()
+                .topData(inArr)
+                .bottomData(outArr)
+                .topStyle(Theme.success())
+                .bottomStyle(Style.EMPTY.fg(Theme.accent()))
+                .showYAxis(true)
+                .xLabels("-" + renderPoints + "s", "-" + (renderPoints * 3 / 
4) + "s",
+                        "-" + (renderPoints / 2) + "s", "-" + (renderPoints / 
4) + "s", "now")
+                
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
+                        
.title(Title.from(Line.from(titleSpans.toArray(Span[]::new)))).build())
+                .build(), area);
+    }
+
+    static void renderPayloadSizeChart(
+            Frame frame, Rect area, LinkedList<Long> inHist, LinkedList<Long> 
outHist) {
+        int renderPoints = Math.max(20, (Math.min(MAX_CHART_POINTS, 
area.width() - 6) / 20) * 20);
+        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] = unbox(inHist.get(idx));
+            }
+            idx = outHist.size() - renderPoints + i;
+            if (idx >= 0) {
+                outArr[i] = unbox(outHist.get(idx));
+            }
+        }
+        long curIn = inArr[renderPoints - 1];
+        long curOut = outArr[renderPoints - 1];
+
+        Line chartTitle = Line.from(
+                Span.styled("▬", Theme.label()),
+                Span.raw(String.format(" in:%-8s ", sizeToString(curIn))),
+                Span.styled("▬", Theme.notice()),
+                Span.raw(String.format(" out:%-8s avg body ", 
sizeToString(curOut))));
+
+        frame.renderWidget(DualSparkline.builder()
+                .topData(inArr)
+                .bottomData(outArr)
+                .topStyle(Theme.label())
+                .bottomStyle(Theme.notice())
+                .showYAxis(true)
+                .xLabels("-" + renderPoints + "s", "-" + (renderPoints * 3 / 
4) + "s",
+                        "-" + (renderPoints / 2) + "s", "-" + (renderPoints / 
4) + "s", "now")
+                
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
+                        .title(Title.from(chartTitle)).build())
+                .build(), area);
+    }
+
+    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));
+        }
+    }
+
+    private static long unbox(Long value) {
+        return value != null ? value : 0L;
+    }
+}
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 88a2b654a942..6f3b6945be2e 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
@@ -89,6 +89,7 @@ class IntegrationInfo {
     final List<RouteControllerInfo> routeControllerRoutes = new ArrayList<>();
     final List<HealthCheckInfo> healthChecks = new ArrayList<>();
     final List<EndpointInfo> endpoints = new ArrayList<>();
+    final List<ServiceInfo> services = new ArrayList<>();
     final List<CircuitBreakerInfo> circuitBreakers = new ArrayList<>();
     final List<KafkaConsumerInfo> kafkaConsumers = new ArrayList<>();
     int errorCount;
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 bd7c71d8496c..63767d61489d 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
@@ -72,6 +72,22 @@ class MetricsCollector {
     private final Map<String, LinkedList<long[]>> perEndpointSamples = new 
ConcurrentHashMap<>();
     private final Map<String, Long> previousPerEndpointTime = new 
ConcurrentHashMap<>();
 
+    // Per-endpoint payload size history — keyed by pid + "|" + uri
+    private final Map<String, LinkedList<Long>> perEndpointInSizeHistory = new 
ConcurrentHashMap<>();
+    private final Map<String, LinkedList<Long>> perEndpointOutSizeHistory = 
new ConcurrentHashMap<>();
+    private final Map<String, Long> previousPerEndpointSizeTime = new 
ConcurrentHashMap<>();
+
+    // Service in/out sliding window history per PID (network services)
+    private final Map<String, LinkedList<Long>> serviceInHistory = new 
ConcurrentHashMap<>();
+    private final Map<String, LinkedList<Long>> serviceOutHistory = new 
ConcurrentHashMap<>();
+    private final Map<String, LinkedList<long[]>> serviceSamples = new 
ConcurrentHashMap<>();
+    private final Map<String, Long> previousServiceTime = new 
ConcurrentHashMap<>();
+
+    // Service payload size (mean body size) history per PID
+    private final Map<String, LinkedList<Long>> serviceInSizeHistory = new 
ConcurrentHashMap<>();
+    private final Map<String, LinkedList<Long>> serviceOutSizeHistory = new 
ConcurrentHashMap<>();
+    private final Map<String, Long> previousServiceSizeTime = new 
ConcurrentHashMap<>();
+
     // Circuit breaker throughput history per PID/cbId
     private final Map<String, LinkedList<Long>> cbSuccessHistory = new 
ConcurrentHashMap<>();
     private final Map<String, LinkedList<Long>> cbFailHistory = new 
ConcurrentHashMap<>();
@@ -140,6 +156,30 @@ class MetricsCollector {
         return perEndpointOutHistory;
     }
 
+    Map<String, LinkedList<Long>> getPerEndpointInSizeHistory() {
+        return perEndpointInSizeHistory;
+    }
+
+    Map<String, LinkedList<Long>> getPerEndpointOutSizeHistory() {
+        return perEndpointOutSizeHistory;
+    }
+
+    Map<String, LinkedList<Long>> getServiceInHistory() {
+        return serviceInHistory;
+    }
+
+    Map<String, LinkedList<Long>> getServiceOutHistory() {
+        return serviceOutHistory;
+    }
+
+    Map<String, LinkedList<Long>> getServiceInSizeHistory() {
+        return serviceInSizeHistory;
+    }
+
+    Map<String, LinkedList<Long>> getServiceOutSizeHistory() {
+        return serviceOutSizeHistory;
+    }
+
     Map<String, LinkedList<Long>> getCbSuccessHistory() {
         return cbSuccessHistory;
     }
@@ -260,6 +300,63 @@ class MetricsCollector {
                     perEndpointSamples, previousPerEndpointTime,
                     perEndpointInHistory, perEndpointOutHistory);
         }
+
+        // Per-endpoint payload size history (keyed by pid|uri)
+        for (EndpointInfo ep : info.endpoints) {
+            if (ep.uri == null || ep.meanBodySize < 0) {
+                continue;
+            }
+            String key = pid + "|" + ep.uri;
+            Long lastTime = previousPerEndpointSizeTime.get(key);
+            if (lastTime == null || now - lastTime >= 1000) {
+                previousPerEndpointSizeTime.put(key, now);
+                if ("in".equals(ep.direction)) {
+                    addToHistory(perEndpointInSizeHistory, key, 
ep.meanBodySize, MAX_ENDPOINT_CHART_POINTS);
+                } else if ("out".equals(ep.direction)) {
+                    addToHistory(perEndpointOutSizeHistory, key, 
ep.meanBodySize, MAX_ENDPOINT_CHART_POINTS);
+                }
+            }
+        }
+    }
+
+    void updateServiceHistory(IntegrationInfo info) {
+        long inTotal = info.services.stream()
+                .filter(s -> "in".equals(s.direction))
+                .mapToLong(s -> s.hits).sum();
+        long outTotal = info.services.stream()
+                .filter(s -> "out".equals(s.direction))
+                .mapToLong(s -> s.hits).sum();
+
+        long now = System.currentTimeMillis();
+        recordEndpointSample(info.pid, now, inTotal, outTotal,
+                serviceSamples, previousServiceTime, serviceInHistory, 
serviceOutHistory);
+
+        // Correlate services with endpoint statistics to get payload size 
data.
+        // Services reference endpointUri which matches the endpoint's uri 
field.
+        Map<String, EndpointInfo> epByUri = new LinkedHashMap<>();
+        for (EndpointInfo ep : info.endpoints) {
+            if (ep.uri != null) {
+                epByUri.put(ep.uri, ep);
+            }
+        }
+        long inMeanSize = 0;
+        long outMeanSize = 0;
+        for (ServiceInfo si : info.services) {
+            EndpointInfo ep = si.endpointUri != null ? 
epByUri.get(si.endpointUri) : null;
+            if (ep != null && ep.meanBodySize >= 0) {
+                if ("in".equals(si.direction)) {
+                    inMeanSize = Math.max(inMeanSize, ep.meanBodySize);
+                } else if ("out".equals(si.direction)) {
+                    outMeanSize = Math.max(outMeanSize, ep.meanBodySize);
+                }
+            }
+        }
+        Long lastSizeTime = previousServiceSizeTime.get(info.pid);
+        if (lastSizeTime == null || now - lastSizeTime >= 1000) {
+            previousServiceSizeTime.put(info.pid, now);
+            addToHistory(serviceInSizeHistory, info.pid, inMeanSize, 
MAX_ENDPOINT_CHART_POINTS);
+            addToHistory(serviceOutSizeHistory, info.pid, outMeanSize, 
MAX_ENDPOINT_CHART_POINTS);
+        }
     }
 
     private void recordEndpointSample(
@@ -368,6 +465,16 @@ class MetricsCollector {
 
         removeByPrefix(pid + "|", perEndpointInHistory, perEndpointOutHistory,
                 perEndpointSamples, previousPerEndpointTime);
+        removeByPrefix(pid + "|", perEndpointInSizeHistory, 
perEndpointOutSizeHistory,
+                previousPerEndpointSizeTime);
+
+        serviceInHistory.remove(pid);
+        serviceOutHistory.remove(pid);
+        serviceSamples.remove(pid);
+        previousServiceTime.remove(pid);
+        serviceInSizeHistory.remove(pid);
+        serviceOutSizeHistory.remove(pid);
+        previousServiceSizeTime.remove(pid);
 
         heapMemHistory.remove(pid);
         previousHeapTime.remove(pid);
@@ -396,6 +503,14 @@ class MetricsCollector {
         previousEndpointSizeTime.remove(pid);
         previousEndpointRemoteStubTime.remove(pid);
 
+        serviceInHistory.remove(pid);
+        serviceOutHistory.remove(pid);
+        serviceSamples.remove(pid);
+        previousServiceTime.remove(pid);
+        serviceInSizeHistory.remove(pid);
+        serviceOutSizeHistory.remove(pid);
+        previousServiceSizeTime.remove(pid);
+
         heapMemHistory.remove(pid);
         previousHeapTime.remove(pid);
         cpuLoadAvg.remove(pid);
@@ -405,6 +520,8 @@ class MetricsCollector {
                 cbThroughputSamples, previousCbTime);
         removeByPrefix(pid + "|", perEndpointInHistory, perEndpointOutHistory,
                 perEndpointSamples, previousPerEndpointTime);
+        removeByPrefix(pid + "|", perEndpointInSizeHistory, 
perEndpointOutSizeHistory,
+                previousPerEndpointSizeTime);
     }
 
     /**
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/NetworkTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/NetworkTab.java
new file mode 100644
index 000000000000..6b925a9b2e2a
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/NetworkTab.java
@@ -0,0 +1,516 @@
+/*
+ * 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.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import dev.tamboui.layout.Constraint;
+import dev.tamboui.layout.Layout;
+import dev.tamboui.layout.Rect;
+import dev.tamboui.style.Style;
+import dev.tamboui.terminal.Frame;
+import dev.tamboui.text.Span;
+import dev.tamboui.tui.event.KeyCode;
+import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.tui.event.MouseEvent;
+import dev.tamboui.widgets.block.Block;
+import dev.tamboui.widgets.block.BorderType;
+import dev.tamboui.widgets.block.Borders;
+import dev.tamboui.widgets.table.Cell;
+import dev.tamboui.widgets.table.Row;
+import dev.tamboui.widgets.table.Table;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+
+import static org.apache.camel.dsl.jbang.core.commands.tui.TuiHelper.*;
+
+class NetworkTab extends AbstractTableTab {
+
+    private static final int CHART_ALL = 0;
+    private static final int CHART_SINGLE = 1;
+    private static final int CHART_OFF = 2;
+
+    private final Map<String, LinkedList<Long>> serviceInHistory;
+    private final Map<String, LinkedList<Long>> serviceOutHistory;
+    private final Map<String, LinkedList<Long>> serviceInSizeHistory;
+    private final Map<String, LinkedList<Long>> serviceOutSizeHistory;
+    private final Map<String, LinkedList<Long>> perEndpointInHistory;
+    private final Map<String, LinkedList<Long>> perEndpointOutHistory;
+    private final Map<String, LinkedList<Long>> perEndpointInSizeHistory;
+    private final Map<String, LinkedList<Long>> perEndpointOutSizeHistory;
+
+    private int chartMode = CHART_ALL;
+    private int chartPanelHeight = 16;
+    private final DragSplit vSplit = new DragSplit();
+    private int flowPanelWidth = 38;
+    private final DragSplit hSplit = new DragSplit();
+
+    NetworkTab(MonitorContext ctx, MetricsCollector metrics) {
+        super(ctx, "component", "route", "dir", "protocol", "hits", "body", 
"hdr", "uri");
+        sortIndex = 1;
+        sort = "route";
+        this.serviceInHistory = metrics.getServiceInHistory();
+        this.serviceOutHistory = metrics.getServiceOutHistory();
+        this.serviceInSizeHistory = metrics.getServiceInSizeHistory();
+        this.serviceOutSizeHistory = metrics.getServiceOutSizeHistory();
+        this.perEndpointInHistory = metrics.getPerEndpointInHistory();
+        this.perEndpointOutHistory = metrics.getPerEndpointOutHistory();
+        this.perEndpointInSizeHistory = metrics.getPerEndpointInSizeHistory();
+        this.perEndpointOutSizeHistory = 
metrics.getPerEndpointOutSizeHistory();
+    }
+
+    @Override
+    protected int getRowCount() {
+        IntegrationInfo info = ctx.findSelectedIntegration();
+        return info != null ? info.services.size() : 0;
+    }
+
+    @Override
+    protected boolean handleTabKeyEvent(KeyEvent ke) {
+        if (ke.isCharIgnoreCase('a')) {
+            chartMode = (chartMode + 1) % 3;
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public boolean handleMouseEvent(MouseEvent me, Rect area) {
+        if (vSplit.handleMouse(me, me.y())) {
+            if (vSplit.isDragging()) {
+                chartPanelHeight = Math.max(5, Math.min(area.y() + 
area.height() - me.y(), area.height() - 5));
+            }
+            return true;
+        }
+        if (chartMode != CHART_OFF && hSplit.handleMouse(me, me.x())) {
+            if (hSplit.isDragging()) {
+                flowPanelWidth = Math.max(20, Math.min(me.x() - area.x(), 
area.width() - 20));
+            }
+            return true;
+        }
+        IntegrationInfo info = ctx.findSelectedIntegration();
+        if (info != null) {
+            if (handleTableClick(me, lastTableArea, tableState, 
info.services.size())) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public void navigateUp() {
+        tableState.selectPrevious();
+    }
+
+    @Override
+    public void navigateDown() {
+        IntegrationInfo info = ctx.findSelectedIntegration();
+        if (info != null) {
+            tableState.selectNext(info.services.size());
+        }
+    }
+
+    @Override
+    public boolean handleKeyEvent(KeyEvent ke) {
+        if (ke.isPageUp() || ke.isKey(KeyCode.PAGE_UP)
+                || ke.isPageDown() || ke.isKey(KeyCode.PAGE_DOWN)
+                || ke.isHome() || ke.isEnd()) {
+            return false;
+        }
+        return super.handleKeyEvent(ke);
+    }
+
+    @Override
+    protected void renderContent(Frame frame, Rect area, IntegrationInfo info) 
{
+        List<ServiceInfo> sorted = new ArrayList<>(info.services);
+        sorted.sort(this::sortService);
+
+        // Build endpoint lookup by URI to get payload size data
+        Map<String, EndpointInfo> epByUri = new java.util.LinkedHashMap<>();
+        for (EndpointInfo ep : info.endpoints) {
+            if (ep.uri != null) {
+                epByUri.put(ep.uri, ep);
+            }
+        }
+
+        boolean hasSize = info.services.stream().anyMatch(si -> {
+            EndpointInfo ep = si.endpointUri != null ? 
epByUri.get(si.endpointUri) : null;
+            return ep != null && (ep.meanBodySize >= 0 || ep.meanHeadersSize 
>= 0);
+        });
+
+        List<Row> rows = new ArrayList<>();
+        for (ServiceInfo si : sorted) {
+            String dir = si.direction != null ? si.direction : "";
+            Style dirStyle = switch (dir) {
+                case "in" -> Theme.success();
+                case "out" -> Style.EMPTY.fg(Theme.accent());
+                default -> Theme.label();
+            };
+            String arrow = switch (dir) {
+                case "in" -> TuiIcons.KEY_RIGHT + " ";
+                case "out" -> TuiIcons.KEY_LEFT + " ";
+                default -> TuiIcons.ARROW_BOTH + " ";
+            };
+
+            List<Cell> cells = new ArrayList<>();
+            cells.add(Cell.from(Span.styled(si.component != null ? 
si.component : "", Style.EMPTY.fg(Theme.accent()))));
+            cells.add(Cell.from(si.routeId != null ? si.routeId : ""));
+            cells.add(Cell.from(Span.styled(arrow + dir, dirStyle)));
+            cells.add(Cell.from(si.protocol != null ? si.protocol : ""));
+            cells.add(centerCell(si.hosted ? "x" : "", 8));
+            cells.add(rightCell(si.hits > 0 ? String.valueOf(si.hits) : "", 
8));
+            if (hasSize) {
+                EndpointInfo ep = si.endpointUri != null ? 
epByUri.get(si.endpointUri) : null;
+                long bodySize = ep != null ? ep.meanBodySize : -1;
+                long hdrSize = ep != null ? ep.meanHeadersSize : -1;
+                cells.add(rightCell(FlowHelper.sizeToString(bodySize), 10));
+                cells.add(rightCell(FlowHelper.sizeToString(hdrSize), 10));
+            }
+            cells.add(Cell.from(si.serviceUrl != null ? si.serviceUrl : ""));
+            rows.add(Row.from(cells));
+        }
+
+        int emptyCols = hasSize ? 9 : 7;
+        if (rows.isEmpty()) {
+            rows.add(emptyRow("No network services", emptyCols));
+        }
+
+        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(Cell.from(Span.styled(sortLabel("PROTOCOL", 
"protocol"), sortStyle("protocol"))));
+        headerCells.add(centerCell("HOSTED", 8, Style.EMPTY.bold()));
+        headerCells.add(rightCell(sortLabel("HITS", "hits"), 8, 
sortStyle("hits")));
+        if (hasSize) {
+            headerCells.add(rightCell(sortLabel("BODY", "body"), 10, 
sortStyle("body")));
+            headerCells.add(rightCell(sortLabel("HDR", "hdr"), 10, 
sortStyle("hdr")));
+        }
+        headerCells.add(Cell.from(Span.styled(sortLabel("SERVICE URL", "uri"), 
sortStyle("uri"))));
+
+        List<Constraint> widths = new ArrayList<>();
+        widths.add(Constraint.length(18));
+        widths.add(Constraint.length(20));
+        widths.add(Constraint.length(8));
+        widths.add(Constraint.length(10));
+        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.fill());
+
+        Table table = Table.builder()
+                .rows(rows)
+                .header(Row.from(headerCells))
+                .widths(widths.toArray(Constraint[]::new))
+                .highlightStyle(Theme.selectionBg())
+                .highlightSpacing(Table.HighlightSpacing.ALWAYS)
+                
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
+                        .title(" Network Services ").build())
+                .build();
+
+        boolean showChart = chartMode != CHART_OFF && ctx.shellPercent < 50;
+        List<Rect> chunks;
+        if (showChart) {
+            chartPanelHeight = Math.max(5, Math.min(chartPanelHeight, 
area.height() - 5));
+            chunks = Layout.vertical().constraints(Constraint.fill(), 
Constraint.length(chartPanelHeight)).split(area);
+            vSplit.setBorderPos(chunks.get(1).y());
+        } else {
+            chunks = List.of(area);
+            vSplit.clearBorderPos();
+            hSplit.clearBorderPos();
+        }
+
+        lastTableArea = chunks.get(0);
+        frame.renderStatefulWidget(table, chunks.get(0), tableState);
+        renderScrollbar(frame, sorted.size());
+
+        if (showChart) {
+            String selectedUri = null;
+            if (chartMode == CHART_SINGLE) {
+                Integer sel = tableState.selected();
+                if (sel != null && sel >= 0 && sel < sorted.size()) {
+                    selectedUri = sorted.get(sel).endpointUri;
+                }
+            }
+
+            if (chartMode == CHART_SINGLE && selectedUri != null) {
+                renderSingleServiceChart(frame, chunks.get(1), selectedUri, 
info);
+            } else {
+                long inTotal = info.services.stream()
+                        .filter(s -> "in".equals(s.direction))
+                        .mapToLong(s -> s.hits).sum();
+                long outTotal = info.services.stream()
+                        .filter(s -> "out".equals(s.direction))
+                        .mapToLong(s -> s.hits).sum();
+
+                boolean hasSizeHistory;
+                try {
+                    hasSizeHistory = serviceInSizeHistory.values().stream()
+                            .anyMatch(h -> new 
ArrayList<>(h).stream().anyMatch(v -> v > 0))
+                            || serviceOutSizeHistory.values().stream()
+                                    .anyMatch(h -> new 
ArrayList<>(h).stream().anyMatch(v -> v > 0));
+                } catch (java.util.ConcurrentModificationException e) {
+                    hasSizeHistory = false;
+                }
+
+                flowPanelWidth = Math.max(20, Math.min(flowPanelWidth, 
chunks.get(1).width() - 20));
+                List<Rect> hParts = Layout.horizontal()
+                        .constraints(Constraint.length(flowPanelWidth), 
Constraint.fill())
+                        .split(chunks.get(1));
+                hSplit.setBorderPos(hParts.get(1).x());
+
+                FlowHelper.renderFlowPanel(frame, hParts.get(0), inTotal, 
outTotal, info.name);
+
+                LinkedList<Long> inHist = 
serviceInHistory.getOrDefault(info.pid, new LinkedList<>());
+                LinkedList<Long> outHist = 
serviceOutHistory.getOrDefault(info.pid, new LinkedList<>());
+
+                if (hasSizeHistory) {
+                    List<Rect> chartSplit = Layout.horizontal()
+                            .constraints(Constraint.percentage(50), 
Constraint.percentage(50))
+                            .split(hParts.get(1));
+                    FlowHelper.renderThroughputChart(frame, chartSplit.get(0), 
inHist, outHist);
+
+                    LinkedList<Long> inSizeHist = 
serviceInSizeHistory.getOrDefault(info.pid, new LinkedList<>());
+                    LinkedList<Long> outSizeHist = 
serviceOutSizeHistory.getOrDefault(info.pid, new LinkedList<>());
+                    FlowHelper.renderPayloadSizeChart(frame, 
chartSplit.get(1), inSizeHist, outSizeHist);
+                } else {
+                    FlowHelper.renderThroughputChart(frame, hParts.get(1), 
inHist, outHist);
+                }
+            }
+        }
+    }
+
+    private void renderSingleServiceChart(Frame frame, Rect area, String 
endpointUri, IntegrationInfo info) {
+        long inTotal = info.services.stream()
+                .filter(s -> "in".equals(s.direction) && 
endpointUri.equals(s.endpointUri))
+                .mapToLong(s -> s.hits).sum();
+        long outTotal = info.services.stream()
+                .filter(s -> "out".equals(s.direction) && 
endpointUri.equals(s.endpointUri))
+                .mapToLong(s -> s.hits).sum();
+
+        flowPanelWidth = Math.max(20, Math.min(flowPanelWidth, area.width() - 
20));
+        List<Rect> hParts = Layout.horizontal()
+                .constraints(Constraint.length(flowPanelWidth), 
Constraint.fill())
+                .split(area);
+        hSplit.setBorderPos(hParts.get(1).x());
+
+        FlowHelper.renderFlowPanel(frame, hParts.get(0), inTotal, outTotal, 
endpointUri);
+
+        String key = info.pid + "|" + endpointUri;
+        LinkedList<Long> inHist = perEndpointInHistory.getOrDefault(key, new 
LinkedList<>());
+        LinkedList<Long> outHist = perEndpointOutHistory.getOrDefault(key, new 
LinkedList<>());
+
+        LinkedList<Long> inSizeHist = 
perEndpointInSizeHistory.getOrDefault(key, new LinkedList<>());
+        LinkedList<Long> outSizeHist = 
perEndpointOutSizeHistory.getOrDefault(key, new LinkedList<>());
+        boolean hasSizeData;
+        try {
+            hasSizeData = new ArrayList<>(inSizeHist).stream().anyMatch(v -> v 
> 0)
+                    || new ArrayList<>(outSizeHist).stream().anyMatch(v -> v > 
0);
+        } catch (java.util.ConcurrentModificationException e) {
+            hasSizeData = false;
+        }
+
+        if (hasSizeData) {
+            List<Rect> chartSplit = Layout.horizontal()
+                    .constraints(Constraint.percentage(50), 
Constraint.percentage(50))
+                    .split(hParts.get(1));
+            FlowHelper.renderThroughputChart(frame, chartSplit.get(0), inHist, 
outHist, endpointUri);
+            FlowHelper.renderPayloadSizeChart(frame, chartSplit.get(1), 
inSizeHist, outSizeHist);
+        } else {
+            FlowHelper.renderThroughputChart(frame, hParts.get(1), inHist, 
outHist, endpointUri);
+        }
+    }
+
+    private long lookupBodySize(ServiceInfo si) {
+        IntegrationInfo info = ctx.findSelectedIntegration();
+        if (info == null || si.endpointUri == null) {
+            return -1;
+        }
+        for (EndpointInfo ep : info.endpoints) {
+            if (si.endpointUri.equals(ep.uri)) {
+                return ep.meanBodySize;
+            }
+        }
+        return -1;
+    }
+
+    private long lookupHeadersSize(ServiceInfo si) {
+        IntegrationInfo info = ctx.findSelectedIntegration();
+        if (info == null || si.endpointUri == null) {
+            return -1;
+        }
+        for (EndpointInfo ep : info.endpoints) {
+            if (si.endpointUri.equals(ep.uri)) {
+                return ep.meanHeadersSize;
+            }
+        }
+        return -1;
+    }
+
+    private int sortService(ServiceInfo a, ServiceInfo b) {
+        int result = switch (sort) {
+            case "component" -> {
+                String ca = a.component != null ? a.component : "";
+                String cb = b.component != null ? b.component : "";
+                yield ca.compareToIgnoreCase(cb);
+            }
+            case "dir" -> {
+                String da = a.direction != null ? a.direction : "";
+                String db = b.direction != null ? b.direction : "";
+                yield da.compareToIgnoreCase(db);
+            }
+            case "protocol" -> {
+                String pa = a.protocol != null ? a.protocol : "";
+                String pb = b.protocol != null ? b.protocol : "";
+                yield pa.compareToIgnoreCase(pb);
+            }
+            case "hits" -> Long.compare(b.hits, a.hits);
+            case "body" -> Long.compare(lookupBodySize(b), lookupBodySize(a));
+            case "hdr" -> Long.compare(lookupHeadersSize(b), 
lookupHeadersSize(a));
+            case "uri" -> {
+                String ua = a.serviceUrl != null ? a.serviceUrl : "";
+                String ub = b.serviceUrl != null ? b.serviceUrl : "";
+                yield ua.compareToIgnoreCase(ub);
+            }
+            default -> { // "route"
+                String ra = a.routeId != null ? a.routeId : "";
+                String rb = b.routeId != null ? b.routeId : "";
+                yield ra.compareToIgnoreCase(rb);
+            }
+        };
+        return sortReversed ? -result : result;
+    }
+
+    @Override
+    public void renderFooter(List<Span> spans) {
+        hint(spans, "Esc", "back");
+        hint(spans, TuiIcons.HINT_SCROLL, "navigate");
+        hint(spans, "s", "sort");
+        String chartLabel = switch (chartMode) {
+            case CHART_ALL -> "[all]";
+            case CHART_SINGLE -> "[single]";
+            default -> "[off]";
+        };
+        hint(spans, "a", "chart " + chartLabel);
+    }
+
+    @Override
+    public SelectionContext getSelectionContext() {
+        IntegrationInfo info = ctx.findSelectedIntegration();
+        if (info == null || info.services.isEmpty()) {
+            return null;
+        }
+        List<ServiceInfo> sorted = new ArrayList<>(info.services);
+        sorted.sort(this::sortService);
+        List<String> items = sorted.stream()
+                .map(s -> s.serviceUrl != null ? s.serviceUrl : "")
+                .toList();
+        Integer sel = tableState.selected();
+        return new SelectionContext("table", items, sel != null ? sel : -1, 
items.size(), "Network Services");
+    }
+
+    @Override
+    public String description() {
+        return "Network-facing services (HTTP listeners, Kafka connections, 
database links)";
+    }
+
+    @Override
+    public String getHelpText() {
+        return """
+                # Network Services
+
+                Network Services shows all **network-facing endpoints** — HTTP 
listeners,
+                Kafka connections, database links, messaging brokers — with 
direction, protocol,
+                and hit counts. Unlike the Endpoints tab which includes 
internal plumbing
+                (`direct:`, `seda:`, `log:`), this tab focuses on real network 
traffic.
+
+                ## Table Columns
+
+                - **COMPONENT** — The Camel component (e.g., `platform-http`, 
`kafka`, `sql`)
+                - **ROUTE** — The route this service belongs to
+                - **DIR** — Direction: `in` (consuming/listening) or `out` 
(producing/calling)
+                - **PROTOCOL** — Network protocol: `http`, `https`, `tcp`, 
`amqp`, etc.
+                - **HOSTED** — Whether this is a locally hosted service (e.g., 
HTTP server) vs a remote client connection
+                - **HITS** — Total number of messages processed through this 
service endpoint
+                - **BODY** — Average message body size (shown when payload 
sizing is active)
+                - **HDR** — Average message headers size (shown when payload 
sizing is active)
+                - **SERVICE URL** — The network address or connection URL
+
+                ## Flow Diagram
+
+                The bottom panel shows the same in/out flow diagram and 
throughput sparkline
+                as the Endpoints tab, but scoped to network services only. 
This gives a
+                clearer picture of actual external traffic without internal 
routing noise.
+
+                ## Keys
+
+                - `Up/Down` — select service
+                - `s` — cycle sort column
+                - `S` — reverse sort order
+                - `a` — toggle chart on/off
+                """;
+    }
+
+    @Override
+    public JsonObject getTableDataAsJson() {
+        IntegrationInfo info = ctx.findSelectedIntegration();
+        if (info == null) {
+            return null;
+        }
+        JsonObject result = new JsonObject();
+        result.put("tab", "Network Services");
+        Map<String, EndpointInfo> epByUri = new java.util.LinkedHashMap<>();
+        for (EndpointInfo ep : info.endpoints) {
+            if (ep.uri != null) {
+                epByUri.put(ep.uri, ep);
+            }
+        }
+        JsonArray rowsArr = new JsonArray();
+        List<ServiceInfo> sorted = new ArrayList<>(info.services);
+        sorted.sort(this::sortService);
+        for (ServiceInfo si : sorted) {
+            JsonObject row = new JsonObject();
+            row.put("component", si.component);
+            row.put("routeId", si.routeId);
+            row.put("direction", si.direction);
+            row.put("protocol", si.protocol);
+            row.put("hosted", si.hosted);
+            row.put("hits", si.hits);
+            EndpointInfo ep = si.endpointUri != null ? 
epByUri.get(si.endpointUri) : null;
+            if (ep != null && ep.meanBodySize >= 0) {
+                row.put("meanBodySize", ep.meanBodySize);
+            }
+            if (ep != null && ep.meanHeadersSize >= 0) {
+                row.put("meanHeadersSize", ep.meanHeadersSize);
+            }
+            row.put("serviceUrl", si.serviceUrl);
+            row.put("endpointUri", si.endpointUri);
+            rowsArr.add(row);
+        }
+        result.put("rows", rowsArr);
+        result.put("totalRows", info.services.size());
+        Integer sel = tableState.selected();
+        result.put("selectedIndex", sel != null ? sel : -1);
+        return result;
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumerInfo.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ServiceInfo.java
similarity index 74%
copy from 
dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumerInfo.java
copy to 
dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ServiceInfo.java
index e9e1ad6f959b..06034e7d67a6 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumerInfo.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ServiceInfo.java
@@ -16,18 +16,13 @@
  */
 package org.apache.camel.dsl.jbang.core.commands.tui;
 
-class ConsumerInfo {
-    String id;
-    String uri;
-    String state;
-    String className;
-    boolean scheduled;
-    int inflight;
-    Boolean polling;
-    Long totalCounter;
-    Long delay;
-    Long period;
-    String sinceLastStarted;
-    String sinceLastCompleted;
-    String sinceLastFailed;
+class ServiceInfo {
+    String component;
+    String direction;
+    boolean hosted;
+    String protocol;
+    String serviceUrl;
+    String endpointUri;
+    String routeId;
+    long hits;
 }
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 db04860676c5..0f6ab05993cc 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
@@ -326,6 +326,34 @@ final class StatusParser {
             }
         }
 
+        // Enrich consumers with quartz trigger schedule data
+        JsonObject quartzObj = (JsonObject) root.get("quartz");
+        if (quartzObj != null) {
+            JsonArray triggers = (JsonArray) quartzObj.get("triggers");
+            if (triggers != null) {
+                for (Object t : triggers) {
+                    JsonObject tj = (JsonObject) t;
+                    String routeId = tj.getString("routeId");
+                    if (routeId != null) {
+                        for (ConsumerInfo ci : info.consumers) {
+                            if (routeId.equals(ci.id)) {
+                                String cron = tj.getString("cron");
+                                if (cron != null) {
+                                    ci.schedule = cron;
+                                } else {
+                                    String interval = 
tj.getString("repeatInterval");
+                                    if (interval != null) {
+                                        ci.schedule = interval;
+                                    }
+                                }
+                                break;
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
         // Parse producers
         JsonObject producersObj = (JsonObject) root.get("producers");
         if (producersObj != null) {
@@ -409,6 +437,27 @@ final class StatusParser {
             }
         }
 
+        // Parse services (network endpoints from service dev console)
+        JsonObject serviceObj = (JsonObject) root.get("services");
+        if (serviceObj != null) {
+            JsonArray serviceList = (JsonArray) serviceObj.get("services");
+            if (serviceList != null) {
+                for (Object s : serviceList) {
+                    JsonObject sj = (JsonObject) s;
+                    ServiceInfo si = new ServiceInfo();
+                    si.component = sj.getString("component");
+                    si.direction = sj.getString("direction");
+                    si.hosted = Boolean.TRUE.equals(sj.get("hosted"));
+                    si.protocol = sj.getString("protocol");
+                    si.serviceUrl = sj.getString("serviceUrl");
+                    si.endpointUri = sj.getString("endpointUri");
+                    si.routeId = sj.getString("routeId");
+                    si.hits = TuiHelper.objToLong(sj.get("hits"));
+                    info.services.add(si);
+                }
+            }
+        }
+
         // Parse circuit breakers: resilience4j, fault-tolerance, core
         parseCbSection(root, "resilience4j", info);
         parseCbSection(root, "fault-tolerance", info);
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
index 5f52269fb948..b88ec9ba9179 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
@@ -74,6 +74,7 @@ class TabRegistry {
     private EventTab eventTab;
     private RouteControllerTab routeControllerTab;
     private EndpointsTab endpointsTab;
+    private NetworkTab networkTab;
     private HttpTab httpTab;
     private SourceTab sourceTab;
     private HealthTab healthTab;
@@ -129,6 +130,7 @@ class TabRegistry {
         sqlQueryTab = new SqlQueryTab(ctx);
         sqlTraceTab = new SqlTraceTab(ctx);
         endpointsTab = new EndpointsTab(ctx, dataService.metrics());
+        networkTab = new NetworkTab(ctx, dataService.metrics());
         httpTab = new HttpTab(ctx);
         sourceTab = new SourceTab(ctx);
         healthTab = new HealthTab(ctx);
@@ -176,6 +178,7 @@ class TabRegistry {
                 // Observability
                 new MoreTab(TuiIcons.TAB_HEALTH, "Health", "H&ealth", 
healthTab, "Observability"),
                 new MoreTab(TuiIcons.TAB_METRICS, "Metrics", "Metr&ics", 
metricsTab, "Observability"),
+                new MoreTab(TuiIcons.TAB_NETWORK, "Network Services", 
"&Network Services", networkTab, "Observability"),
                 new MoreTab(TuiIcons.TAB_EVENTS, "Events", "E&xchange Events", 
eventTab, "Observability"),
                 new MoreTab(TuiIcons.TAB_SPANS, "Spans", "&OTel Spans", 
spansTab, "Observability"),
                 // Data
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIcons.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIcons.java
index 3e96996b5b6c..32d95332f8e4 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIcons.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIcons.java
@@ -140,6 +140,7 @@ final class TuiIcons {
     static final String TAB_MAVEN_DEPENDENCIES = "📜";
     static final String TAB_MEMORY = MEMORY;
     static final String TAB_MEMORY_LEAK = "💧";
+    static final String TAB_NETWORK = "🌐";
     static final String TAB_METRICS = "📈";
     static final String TAB_SQL_QUERY = KEY;
     static final String TAB_SQL_TRACE = "🔎";
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumersTabRenderTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumersTabRenderTest.java
index 38940bac9973..c5bfc716773a 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumersTabRenderTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumersTabRenderTest.java
@@ -206,6 +206,27 @@ class ConsumersTabRenderTest {
         assertTrue(footer.contains("sort"), "Footer should contain sort hint");
     }
 
+    @Test
+    void serverProvidedSchedulePreferred() {
+        ConsumerInfo ci = addConsumer("quartz-route", 
"quartz://myGroup/myTimer?cron=0+0/5+*+*+*+?", "Started",
+                "org.apache.camel.component.quartz.QuartzConsumer");
+        ci.schedule = "0 0/1 * * * ?";
+
+        String schedule = ConsumersTab.consumerSchedule(ci);
+        assertTrue(schedule.equals("0 0/1 * * * ?"),
+                "Server-provided schedule should take precedence over URI 
parsing");
+    }
+
+    @Test
+    void quartzCronExtractedFromUri() {
+        ConsumerInfo ci = addConsumer("quartz-route", 
"quartz://myGroup/myTimer?cron=0+0/5+*+*+*+?", "Started",
+                "org.apache.camel.component.quartz.QuartzConsumer");
+
+        String schedule = ConsumersTab.consumerSchedule(ci);
+        assertTrue(schedule.equals("0 0/5 * * * ?"),
+                "Cron should be extracted and decoded from quartz URI");
+    }
+
     // ---- Helper methods ----
 
     private ConsumerInfo addConsumer(String id, String uri, String state, 
String className) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistryTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistryTest.java
index 6b384a904836..8d6c332b8f4e 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistryTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistryTest.java
@@ -82,7 +82,7 @@ class TabRegistryTest {
 
     @Test
     void moreTabsHasTwentySevenEntries() {
-        assertEquals(27, registry.moreTabs().size());
+        assertEquals(28, registry.moreTabs().size());
     }
 
     @Test
@@ -108,7 +108,7 @@ class TabRegistryTest {
         // MORE_SHORTCUTS array carried before the MoreTab refactor. A label 
edit that repoints a key must fail here.
         List<Character> shortcuts = 
registry.moreTabs().stream().map(TabRegistry.MoreTab::shortcut).toList();
         assertEquals(
-                List.of('W', 'C', 'N', 'H', 'F', 'U', 'Z', 'E', 'I', 'X', 'O', 
'J', 'K', 'Q', 'R', 'A', 'H', 'M', 'Y',
+                List.of('W', 'C', 'N', 'H', 'F', 'U', 'Z', 'E', 'I', 'N', 'X', 
'O', 'J', 'K', 'Q', 'R', 'A', 'H', 'M', 'Y',
                         'P', 'S', 'T', 'B', 'L', 'G', 'V', 'D'),
                 shortcuts, "More tab shortcut letters must match the 
historical sequence");
     }

Reply via email to