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 4172b3d6762b CAMEL-23514: Add metric counters to ASCII/Unicode diagram 
(#23206)
4172b3d6762b is described below

commit 4172b3d6762b4329b766c845587f1976a3985145
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu May 14 11:03:42 2026 +0200

    CAMEL-23514: Add metric counters to ASCII/Unicode diagram (#23206)
    
    * CAMEL-23514: Add metric counters to ASCII/Unicode diagram
    
    Add metrics support to RouteDiagramAsciiRenderer matching the image
    renderer: success counter on the right, failure counter on the left
    of each arrow tip, with dashed arrows for zero-traffic paths.
    
    Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    * CAMEL-23514: Pass metrics flag to ASCII renderer in TUI monitor
    
    Parse statistics from route-structure JSON response and pass
    metrics=true to RouteDiagramAsciiRenderer so counters are shown
    in the text diagram view.
    
    Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    * CAMEL-23514: Pass metrics flag to ASCII renderer in CLI route-diagram 
command
    
    The --metric flag was only passed to the image renderer. Now also
    passed to RouteDiagramAsciiRenderer for ascii/unicode themes.
    
    Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    * CAMEL-23514: Add metrics toggle to TUI diagram view
    
    Add 'm' key to toggle metrics on/off in the diagram view. When
    metrics are enabled and showing a text diagram, the diagram is
    automatically refreshed on each tick so counters update in real time.
    
    Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    * CAMEL-23514: Fix diagram flicker during live metric refresh
    
    Only show the "Loading diagram..." state on initial load, not on
    tick-based refreshes. Preserve scroll position when refreshing an
    already-visible diagram. This prevents the visible flash between
    the loading placeholder and the actual diagram content on each tick.
    
    Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    * CAMEL-23514: Fix metrics toggle not taking effect immediately
    
    Reset diagramLoading guard before reloading so the toggle is not
    blocked by an in-progress tick-triggered load that captured the
    old metrics flag.
    
    Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    * CAMEL-23514: Remove refresh interval from footer
    
    The refresh interval is not user-configurable and adds no value
    to the footer display.
    
    Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    * CAMEL-23514: Add metrics to image diagram with F5 manual refresh
    
    Pass metrics flag to RouteDiagramRenderer for image diagrams so
    counters are shown. Add F5 key to manually refresh the diagram
    (both text and image modes) to update counters on demand. The
    image diagram shows static counters from load time since terminal
    image protocols don't support partial updates. Footer hints show
    F5 refresh when metrics are enabled.
    
    Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    * CAMEL-23514: Limit F5 refresh to image diagram only
    
    Text diagram auto-refreshes on tick so F5 is redundant there.
    Only show F5 hint in image diagram mode with metrics enabled.
    
    Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    * CAMEL-23514: Remove redundant r=refresh key and hint
    
    The overview tab auto-refreshes every 100ms via tick, making the
    manual r=refresh key unnecessary.
    
    Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    * CAMEL-23514: Add colored counters to text and image diagrams
    
    Add counter position tracking to RouteDiagramAsciiRenderer so callers
    can colorize success (green) and failure (red) counters.
    
    - Renderer records CounterPos (row, col, length, type) for each counter
    - renderDiagramAnsi() applies ANSI color codes for CLI terminal output
    - TUI styleDiagramLine() uses positions to create colored Spans
    - CLI route-diagram command uses ANSI-colored output
    
    Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    * CAMEL-23514: Fix counter color positions after empty line removal
    
    Counter positions from the ASCII renderer use original grid row
    numbers, but empty lines are filtered out when building diagramLines.
    Remap the row indices to account for removed lines so counter
    coloring matches the correct positions in the TUI.
    
    Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    * CAMEL-23514: Fix TUI counter coloring not applied
    
    The styleDiagramLine parser was outputting entire lines as WHITE when
    no [tag] brackets were found, skipping the counter range check. Now
    all text segments go through addStyledSegment which scans for counter
    ranges. Also fixed findNextCounterRange to find counters starting at
    or after the current position, not just those containing the position.
    
    Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    ---------
    
    Signed-off-by: Claus Ibsen <[email protected]>
    Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
---
 .../camel/diagram/RouteDiagramAsciiRenderer.java   |  97 ++++++++-
 .../org/apache/camel/diagram/RouteDiagramTest.java |  81 ++++++++
 .../commands/action/CamelRouteDiagramAction.java   |   4 +-
 .../dsl/jbang/core/commands/tui/CamelMonitor.java  | 219 ++++++++++++++-------
 4 files changed, 326 insertions(+), 75 deletions(-)

diff --git 
a/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramAsciiRenderer.java
 
b/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramAsciiRenderer.java
index 853c08ec7cd4..5d0f9bb0173a 100644
--- 
a/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramAsciiRenderer.java
+++ 
b/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramAsciiRenderer.java
@@ -23,8 +23,10 @@ import java.util.List;
 import org.apache.camel.diagram.RouteDiagramLayoutEngine.Bounds;
 import org.apache.camel.diagram.RouteDiagramLayoutEngine.LayoutNode;
 import org.apache.camel.diagram.RouteDiagramLayoutEngine.LayoutRoute;
+import org.apache.camel.diagram.RouteDiagramLayoutEngine.StatInfo;
 import org.apache.camel.diagram.RouteDiagramLayoutEngine.TreeNode;
 
+import static 
org.apache.camel.diagram.RouteDiagramLayoutEngine.BRANCH_CHILD_TYPES;
 import static org.apache.camel.diagram.RouteDiagramLayoutEngine.PADDING;
 import static org.apache.camel.diagram.RouteDiagramLayoutEngine.SCOPE_BOX_PAD;
 
@@ -55,22 +57,63 @@ public class RouteDiagramAsciiRenderer {
     private final int nodeWidth;
     private final int boxWidth;
     private final boolean unicode;
+    private final boolean metrics;
+    private final List<CounterPos> counterPositions = new ArrayList<>();
+
+    public enum CounterType {
+        OK,
+        FAIL
+    }
+
+    public record CounterPos(int row, int col, int length, CounterType type) {
+    }
 
     public RouteDiagramAsciiRenderer(int nodeWidth) {
-        this(nodeWidth, false);
+        this(nodeWidth, false, false);
     }
 
     public RouteDiagramAsciiRenderer(int nodeWidth, boolean unicode) {
+        this(nodeWidth, unicode, false);
+    }
+
+    public RouteDiagramAsciiRenderer(int nodeWidth, boolean unicode, boolean 
metrics) {
         this.nodeWidth = nodeWidth;
         this.boxWidth = Math.max(MIN_BOX_WIDTH, nodeWidth / X_DIVISOR);
         this.unicode = unicode;
+        this.metrics = metrics;
     }
 
     public int getBoxWidth() {
         return boxWidth;
     }
 
+    public List<CounterPos> getCounterPositions() {
+        return counterPositions;
+    }
+
+    public String renderDiagramAnsi(List<LayoutRoute> layoutRoutes, int 
totalHeight) {
+        String plain = renderDiagram(layoutRoutes, totalHeight);
+        if (counterPositions.isEmpty()) {
+            return plain;
+        }
+        String[] lines = plain.split("\n", -1);
+        for (CounterPos cp : counterPositions) {
+            if (cp.row >= 0 && cp.row < lines.length) {
+                String line = lines[cp.row];
+                if (cp.col >= 0 && cp.col + cp.length <= line.length()) {
+                    String before = line.substring(0, cp.col);
+                    String counter = line.substring(cp.col, cp.col + 
cp.length);
+                    String after = line.substring(cp.col + cp.length);
+                    String color = cp.type == CounterType.OK ? "\033[32m" : 
"\033[31m";
+                    lines[cp.row] = before + color + counter + "\033[0m" + 
after;
+                }
+            }
+        }
+        return String.join("\n", lines);
+    }
+
     public String renderDiagram(List<LayoutRoute> layoutRoutes, int 
totalHeight) {
+        counterPositions.clear();
         int maxPixelX = layoutRoutes.stream()
                 .mapToInt(lr -> lr.maxX).max().orElse(nodeWidth) + PADDING;
         int gridWidth = toCol(maxPixelX) + boxWidth + 4;
@@ -166,7 +209,12 @@ public class RouteDiagramAsciiRenderer {
         int toCx = centerCol(to);
         int toTop = getTopRow(to);
 
-        drawArrowPath(grid, fromCx, fromBottom, toCx, toTop);
+        StatInfo stat = resolveStatInfo(to);
+        long total = stat != null ? stat.exchangesTotal : 0;
+        boolean dashed = metrics && total == 0;
+
+        drawArrowPath(grid, fromCx, fromBottom, toCx, toTop, dashed);
+        drawCounters(grid, toCx, toTop, stat);
     }
 
     private void drawMergeArrow(char[][] grid, LayoutNode to) {
@@ -175,16 +223,53 @@ public class RouteDiagramAsciiRenderer {
         int toCx = centerCol(to);
         int toTop = getTopRow(to);
 
-        drawArrowPath(grid, fromCx, fromRow, toCx, toTop);
+        StatInfo stat = metrics ? to.treeNode.info.stat : null;
+        long total = stat != null ? stat.exchangesTotal : 0;
+        boolean dashed = metrics && total == 0;
+
+        drawArrowPath(grid, fromCx, fromRow, toCx, toTop, dashed);
+        drawCounters(grid, toCx, toTop, stat);
+    }
+
+    private StatInfo resolveStatInfo(LayoutNode to) {
+        if (!metrics) {
+            return null;
+        }
+        StatInfo stat = to.treeNode.info.stat;
+        if (BRANCH_CHILD_TYPES.contains(to.type) && 
!to.treeNode.children.isEmpty()) {
+            stat = to.treeNode.children.get(0).info.stat;
+        }
+        return stat;
+    }
+
+    private void drawCounters(char[][] grid, int toCx, int toTop, StatInfo 
stat) {
+        if (!metrics || stat == null) {
+            return;
+        }
+        long total = stat.exchangesTotal;
+        long failed = stat.exchangesFailed;
+        long ok = total - failed;
+        if (ok > 0) {
+            String okStr = "" + ok;
+            int col = toCx + 2;
+            drawText(grid, toTop - 1, col, okStr);
+            counterPositions.add(new CounterPos(toTop - 1, col, 
okStr.length(), CounterType.OK));
+        }
+        if (failed > 0) {
+            String failStr = "" + failed;
+            int col = toCx - 1 - failStr.length();
+            drawText(grid, toTop - 1, col, failStr);
+            counterPositions.add(new CounterPos(toTop - 1, col, 
failStr.length(), CounterType.FAIL));
+        }
     }
 
-    private void drawArrowPath(char[][] grid, int fromCx, int fromRow, int 
toCx, int toRow) {
+    private void drawArrowPath(char[][] grid, int fromCx, int fromRow, int 
toCx, int toRow, boolean dashed) {
         if (fromRow >= toRow) {
             return;
         }
 
-        char v = unicode ? UNI_V : '|';
-        char h = unicode ? UNI_H : '-';
+        char v = dashed ? (unicode ? UNI_DASH_V : ':') : (unicode ? UNI_V : 
'|');
+        char h = dashed ? (unicode ? UNI_DASH_H : '.') : (unicode ? UNI_H : 
'-');
         char arrow = unicode ? UNI_ARROW : 'v';
 
         if (fromCx == toCx) {
diff --git 
a/components/camel-diagram/src/test/java/org/apache/camel/diagram/RouteDiagramTest.java
 
b/components/camel-diagram/src/test/java/org/apache/camel/diagram/RouteDiagramTest.java
index b6ab0d9fd7d8..0684c1430754 100644
--- 
a/components/camel-diagram/src/test/java/org/apache/camel/diagram/RouteDiagramTest.java
+++ 
b/components/camel-diagram/src/test/java/org/apache/camel/diagram/RouteDiagramTest.java
@@ -24,6 +24,7 @@ import 
org.apache.camel.diagram.RouteDiagramLayoutEngine.LayoutNode;
 import org.apache.camel.diagram.RouteDiagramLayoutEngine.LayoutRoute;
 import org.apache.camel.diagram.RouteDiagramLayoutEngine.NodeInfo;
 import org.apache.camel.diagram.RouteDiagramLayoutEngine.RouteInfo;
+import org.apache.camel.diagram.RouteDiagramLayoutEngine.StatInfo;
 import org.apache.camel.diagram.RouteDiagramLayoutEngine.TreeNode;
 import org.apache.camel.diagram.RouteDiagramRenderer.DiagramColors;
 import org.junit.jupiter.api.Test;
@@ -1058,6 +1059,77 @@ class RouteDiagramTest {
         assertTrue(lines.get(lines.size() - 1).endsWith("..."), "Truncated 
text should end with ...");
     }
 
+    @Test
+    void testAsciiDiagramWithMetrics() {
+        RouteInfo route = new RouteInfo();
+        route.routeId = "route1";
+        route.nodes.add(nodeWithStat("from", "timer:tick", 0, 100, 0));
+        route.nodes.add(nodeWithStat("to", "log:a", 1, 95, 5));
+        route.nodes.add(nodeWithStat("to", "log:b", 1, 90, 0));
+
+        RouteDiagramLayoutEngine engine = new RouteDiagramLayoutEngine();
+        LayoutRoute lr = engine.layoutRoute(route, 
RouteDiagramLayoutEngine.PADDING);
+
+        RouteDiagramAsciiRenderer renderer = new 
RouteDiagramAsciiRenderer(engine.getNodeWidth(), false, true);
+        String result = renderer.renderDiagram(List.of(lr), lr.maxY + 
RouteDiagramLayoutEngine.V_GAP);
+
+        assertTrue(result.contains("90"), "Should show success counter for 
log:a (95-5=90)");
+        assertTrue(result.contains("5"), "Should show failure counter for 
log:a");
+        assertTrue(result.contains("90"), "Should show success counter for 
log:b");
+    }
+
+    @Test
+    void testAsciiDiagramMetricsDashedArrowForZeroTraffic() {
+        RouteInfo route = new RouteInfo();
+        route.routeId = "route1";
+        route.nodes.add(nodeWithStat("from", "timer:tick", 0, 0, 0));
+        route.nodes.add(nodeWithStat("to", "log:a", 1, 0, 0));
+
+        RouteDiagramLayoutEngine engine = new RouteDiagramLayoutEngine();
+        LayoutRoute lr = engine.layoutRoute(route, 
RouteDiagramLayoutEngine.PADDING);
+
+        RouteDiagramAsciiRenderer renderer = new 
RouteDiagramAsciiRenderer(engine.getNodeWidth(), false, true);
+        String result = renderer.renderDiagram(List.of(lr), lr.maxY + 
RouteDiagramLayoutEngine.V_GAP);
+
+        assertTrue(result.contains(":"), "Should contain dashed vertical line 
for zero-traffic arrow");
+    }
+
+    @Test
+    void testUnicodeDiagramWithMetrics() {
+        RouteInfo route = new RouteInfo();
+        route.routeId = "route1";
+        route.nodes.add(nodeWithStat("from", "timer:tick", 0, 50, 0));
+        route.nodes.add(nodeWithStat("to", "log:a", 1, 50, 2));
+
+        RouteDiagramLayoutEngine engine = new RouteDiagramLayoutEngine();
+        LayoutRoute lr = engine.layoutRoute(route, 
RouteDiagramLayoutEngine.PADDING);
+
+        RouteDiagramAsciiRenderer renderer = new 
RouteDiagramAsciiRenderer(engine.getNodeWidth(), true, true);
+        String result = renderer.renderDiagram(List.of(lr), lr.maxY + 
RouteDiagramLayoutEngine.V_GAP);
+
+        assertTrue(result.contains("48"), "Should show success counter 
(50-2=48)");
+        assertTrue(result.contains("2"), "Should show failure counter");
+    }
+
+    @Test
+    void testAsciiDiagramMetricsNoCountersWithoutFlag() {
+        RouteInfo route = new RouteInfo();
+        route.routeId = "route1";
+        route.nodes.add(nodeWithStat("from", "timer:tick", 0, 100, 0));
+        route.nodes.add(nodeWithStat("to", "log:a", 1, 100, 10));
+
+        RouteDiagramLayoutEngine engine = new RouteDiagramLayoutEngine();
+        LayoutRoute lr = engine.layoutRoute(route, 
RouteDiagramLayoutEngine.PADDING);
+
+        RouteDiagramAsciiRenderer renderer = new 
RouteDiagramAsciiRenderer(engine.getNodeWidth(), false, false);
+        String result = renderer.renderDiagram(List.of(lr), lr.maxY + 
RouteDiagramLayoutEngine.V_GAP);
+
+        // The counters 90 and 10 should not appear when metrics=false
+        // (the numbers might appear in other contexts, but not as standalone 
counters near arrows)
+        assertTrue(result.contains("timer:tick"));
+        assertTrue(result.contains("log:a"));
+    }
+
     private static NodeInfo node(String type, String code, int level) {
         return node(type, code, level, null);
     }
@@ -1070,4 +1142,13 @@ class RouteDiagramTest {
         n.level = level;
         return n;
     }
+
+    private static NodeInfo nodeWithStat(String type, String code, int level, 
long total, long failed) {
+        NodeInfo n = node(type, code, level);
+        StatInfo stat = new StatInfo();
+        stat.exchangesTotal = total;
+        stat.exchangesFailed = failed;
+        n.stat = stat;
+        return n;
+    }
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteDiagramAction.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteDiagramAction.java
index cde562c5235a..c29cb7b8df37 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteDiagramAction.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteDiagramAction.java
@@ -211,8 +211,8 @@ public class CamelRouteDiagramAction extends 
ActionWatchCommand {
 
             if (isTextTheme()) {
                 RouteDiagramAsciiRenderer asciiRenderer
-                        = new RouteDiagramAsciiRenderer(engine.getNodeWidth(), 
isUnicodeTheme());
-                String ascii = asciiRenderer.renderDiagram(layoutRoutes, 
currentY);
+                        = new RouteDiagramAsciiRenderer(engine.getNodeWidth(), 
isUnicodeTheme(), pid > 0 && metric);
+                String ascii = asciiRenderer.renderDiagramAnsi(layoutRoutes, 
currentY);
 
                 if (output != null) {
                     String fileName = output.endsWith(".png")
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 0984a3e65fff..f033cb79884a 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
@@ -57,6 +57,7 @@ import dev.tamboui.text.Text;
 import dev.tamboui.tui.TuiConfig;
 import dev.tamboui.tui.TuiRunner;
 import dev.tamboui.tui.event.Event;
+import dev.tamboui.tui.event.KeyCode;
 import dev.tamboui.tui.event.KeyEvent;
 import dev.tamboui.tui.event.MouseEvent;
 import dev.tamboui.tui.event.MouseEventKind;
@@ -176,6 +177,8 @@ public class CamelMonitor extends CamelCommand {
     // Diagram state
     private boolean showDiagram;
     private boolean diagramTextMode;
+    private boolean diagramMetrics = true;
+    private List<RouteDiagramAsciiRenderer.CounterPos> diagramCounterPositions 
= Collections.emptyList();
     private List<String> diagramLines = Collections.emptyList();
     private int diagramScroll;
     private int diagramScrollX;
@@ -260,11 +263,6 @@ public class CamelMonitor extends CamelCommand {
                 runner.quit();
                 return true;
             }
-            if (ke.isChar('r')) {
-                refreshData();
-                return true;
-            }
-
             // Tab switching with number keys
             if (ke.isChar('1')) {
                 return handleTabKey(TAB_OVERVIEW);
@@ -400,6 +398,18 @@ public class CamelMonitor extends CamelCommand {
                 return true;
             }
 
+            if (tab == TAB_ROUTES && showDiagram && ke.isCharIgnoreCase('m')) {
+                diagramMetrics = !diagramMetrics;
+                diagramLoading.set(false);
+                loadDiagramForSelectedRoute();
+                return true;
+            }
+            if (tab == TAB_ROUTES && showDiagram && !diagramTextMode && 
ke.isKey(KeyCode.F5)) {
+                diagramLoading.set(false);
+                loadDiagramForSelectedRoute();
+                return true;
+            }
+
             // Health tab: DOWN filter
             if (tab == TAB_HEALTH && ke.isCharIgnoreCase('d')) {
                 showOnlyDown = !showOnlyDown;
@@ -475,6 +485,9 @@ public class CamelMonitor extends CamelCommand {
             long interval = showDiagram ? Math.max(refreshInterval, 1000) : 
refreshInterval;
             if (now - lastRefresh >= interval) {
                 refreshData();
+                if (showDiagram && diagramTextMode && diagramMetrics) {
+                    loadDiagramForSelectedRoute();
+                }
                 return true;
             }
             // Skip re-render when showing image diagram to prevent flicker
@@ -1042,7 +1055,7 @@ public class CamelMonitor extends CamelCommand {
             if (diagramScrollX > 0) {
                 line = diagramScrollX < line.length() ? 
line.substring(diagramScrollX) : "";
             }
-            lines.add(styleDiagramLine(line));
+            lines.add(styleDiagramLine(line, i, diagramScrollX));
         }
 
         // Layout: outer block wraps everything, inner splits content + 
scrollbars
@@ -1163,22 +1176,37 @@ public class CamelMonitor extends CamelCommand {
         }
     }
 
-    private Line styleDiagramLine(String text) {
+    private Line styleDiagramLine(String text, int row, int scrollX) {
+        // Build counter color ranges for this row
+        List<int[]> counterRanges = new ArrayList<>();
+        for (RouteDiagramAsciiRenderer.CounterPos cp : 
diagramCounterPositions) {
+            if (cp.row() == row) {
+                int start = cp.col() - scrollX;
+                int end = start + cp.length();
+                if (end > 0 && start < text.length()) {
+                    start = Math.max(0, start);
+                    end = Math.min(end, text.length());
+                    int colorFlag = cp.type() == 
RouteDiagramAsciiRenderer.CounterType.OK ? 1 : 2;
+                    counterRanges.add(new int[] { start, end, colorFlag });
+                }
+            }
+        }
+
         List<Span> spans = new ArrayList<>();
         int idx = 0;
         while (idx < text.length()) {
             int open = text.indexOf('[', idx);
             if (open < 0) {
-                spans.add(Span.styled(text.substring(idx), 
Style.EMPTY.fg(Color.WHITE)));
+                addStyledSegment(spans, text, idx, text.length(), 
counterRanges, Color.WHITE);
                 break;
             }
             int close = text.indexOf(']', open);
             if (close < 0) {
-                spans.add(Span.styled(text.substring(idx), 
Style.EMPTY.fg(Color.WHITE)));
+                addStyledSegment(spans, text, idx, text.length(), 
counterRanges, Color.WHITE);
                 break;
             }
             if (open > idx) {
-                spans.add(Span.styled(text.substring(idx, open), 
Style.EMPTY.fg(Color.GRAY)));
+                addStyledSegment(spans, text, idx, open, counterRanges, 
Color.GRAY);
             }
             String tag = text.substring(open + 1, close);
             Color tagColor = getDiagramNodeColor(tag);
@@ -1186,16 +1214,48 @@ public class CamelMonitor extends CamelCommand {
 
             int afterTag = close + 1;
             int nextOpen = text.indexOf('[', afterTag);
-            int nextNewline = text.length();
-            int labelEnd = nextOpen >= 0 ? nextOpen : nextNewline;
+            int labelEnd = nextOpen >= 0 ? nextOpen : text.length();
             if (afterTag < labelEnd) {
-                spans.add(Span.styled(text.substring(afterTag, labelEnd), 
Style.EMPTY.fg(Color.WHITE)));
+                addStyledSegment(spans, text, afterTag, labelEnd, 
counterRanges, Color.WHITE);
             }
             idx = labelEnd;
         }
         return Line.from(spans);
     }
 
+    private void addStyledSegment(
+            List<Span> spans, String text, int from, int to, List<int[]> 
counterRanges, Color defaultColor) {
+        int pos = from;
+        while (pos < to) {
+            int[] cr = findNextCounterRange(counterRanges, pos, to);
+            if (cr != null) {
+                if (pos < cr[0]) {
+                    spans.add(Span.styled(text.substring(pos, cr[0]), 
Style.EMPTY.fg(defaultColor)));
+                }
+                int counterEnd = Math.min(cr[1], to);
+                Color counterColor = cr[2] == 1 ? Color.GREEN : Color.RED;
+                spans.add(Span.styled(text.substring(cr[0], counterEnd), 
Style.EMPTY.fg(counterColor).bold()));
+                pos = counterEnd;
+            } else {
+                spans.add(Span.styled(text.substring(pos, to), 
Style.EMPTY.fg(defaultColor)));
+                pos = to;
+            }
+        }
+    }
+
+    private static int[] findNextCounterRange(List<int[]> ranges, int pos, int 
limit) {
+        int[] best = null;
+        for (int[] range : ranges) {
+            if (range[1] > pos && range[0] < limit) {
+                int start = Math.max(range[0], pos);
+                if (best == null || start < best[0]) {
+                    best = new int[] { start, range[1], range[2] };
+                }
+            }
+        }
+        return best;
+    }
+
     private Color getDiagramNodeColor(String type) {
         if (type == null) {
             return Color.GRAY;
@@ -1246,27 +1306,30 @@ public class CamelMonitor extends CamelCommand {
         // Capture state needed by the background thread
         String pid = selectedPid;
         boolean textMode = diagramTextMode;
+        boolean showMetrics = diagramMetrics;
         String routeId = selectedRoute.routeId;
 
-        // Show loading state immediately
-        diagramRouteId = routeId;
-        diagramLines = List.of("(Loading diagram...)");
-        diagramImageData = null;
-        diagramFullImageData = null;
-        showDiagram = true;
-        diagramScroll = 0;
-        diagramScrollX = 0;
+        boolean initialLoad = !showDiagram;
+        if (initialLoad) {
+            diagramRouteId = routeId;
+            diagramLines = List.of("(Loading diagram...)");
+            diagramImageData = null;
+            diagramFullImageData = null;
+            showDiagram = true;
+            diagramScroll = 0;
+            diagramScrollX = 0;
+        }
 
         runner.scheduler().execute(() -> {
             try {
-                loadDiagramInBackground(pid, textMode, routeId);
+                loadDiagramInBackground(pid, textMode, routeId, showMetrics);
             } finally {
                 diagramLoading.set(false);
             }
         });
     }
 
-    private void loadDiagramInBackground(String pid, boolean textMode, String 
routeId) {
+    private void loadDiagramInBackground(String pid, boolean textMode, String 
routeId, boolean metrics) {
         Path outputFile = getOutputFile(pid);
         PathUtils.deleteFile(outputFile);
 
@@ -1310,6 +1373,13 @@ public class CamelMonitor extends CamelCommand {
                     node.code = Jsoner.unescape(objToString(line.get("code")));
                     Integer level = line.getInteger("level");
                     node.level = level != null ? level : 0;
+                    JsonObject stats = (JsonObject) line.get("statistics");
+                    if (stats != null) {
+                        RouteDiagramLayoutEngine.StatInfo stat = new 
RouteDiagramLayoutEngine.StatInfo();
+                        stat.exchangesTotal = 
stats.getLongOrDefault("exchangesTotal", 0);
+                        stat.exchangesFailed = 
stats.getLongOrDefault("exchangesFailed", 0);
+                        node.stat = stat;
+                    }
                     route.nodes.add(node);
                 }
             }
@@ -1317,14 +1387,42 @@ public class CamelMonitor extends CamelCommand {
         }
 
         if (textMode) {
-            String ascii = renderAscii(diagramRoutes, 
RouteDiagramLayoutEngine.DEFAULT_BOX_WIDTH, "CODE", true);
+            RouteDiagramLayoutEngine engine = new RouteDiagramLayoutEngine(
+                    RouteDiagramLayoutEngine.DEFAULT_BOX_WIDTH, 
RouteDiagramLayoutEngine.DEFAULT_FONT_SIZE,
+                    RouteDiagramLayoutEngine.NodeLabelMode.CODE);
+            List<RouteDiagramLayoutEngine.LayoutRoute> layoutRoutes = new 
ArrayList<>();
+            int currentY = RouteDiagramLayoutEngine.PADDING;
+            for (RouteDiagramLayoutEngine.RouteInfo r : diagramRoutes) {
+                RouteDiagramLayoutEngine.LayoutRoute lr = 
engine.layoutRoute(r, currentY);
+                layoutRoutes.add(lr);
+                currentY = lr.maxY + RouteDiagramLayoutEngine.V_GAP;
+            }
+            RouteDiagramAsciiRenderer asciiRenderer = new 
RouteDiagramAsciiRenderer(
+                    RouteDiagramLayoutEngine.DEFAULT_BOX_WIDTH * 
RouteDiagramLayoutEngine.SCALE, true, metrics);
+            String ascii = asciiRenderer.renderDiagram(layoutRoutes, currentY);
+            List<RouteDiagramAsciiRenderer.CounterPos> origPositions = 
asciiRenderer.getCounterPositions();
+
+            // Build result lines, remapping counter positions to account for 
removed empty lines
+            String[] rawLines = ascii.split("\n", -1);
             List<String> result = new ArrayList<>();
-            for (String line : ascii.split("\n", -1)) {
-                if (!line.isEmpty()) {
-                    result.add(line);
+            int[] rowMapping = new int[rawLines.length];
+            int newRow = 0;
+            for (int i = 0; i < rawLines.length; i++) {
+                if (!rawLines[i].isEmpty()) {
+                    rowMapping[i] = newRow++;
+                    result.add(rawLines[i]);
+                } else {
+                    rowMapping[i] = -1;
+                }
+            }
+            List<RouteDiagramAsciiRenderer.CounterPos> positions = new 
ArrayList<>();
+            for (RouteDiagramAsciiRenderer.CounterPos cp : origPositions) {
+                if (cp.row() >= 0 && cp.row() < rowMapping.length && 
rowMapping[cp.row()] >= 0) {
+                    positions.add(new RouteDiagramAsciiRenderer.CounterPos(
+                            rowMapping[cp.row()], cp.col(), cp.length(), 
cp.type()));
                 }
             }
-            applyDiagramResult(routeId, result, null, null, null);
+            applyDiagramResult(routeId, result, null, null, null, positions);
         } else {
             TerminalImageCapabilities caps = 
TerminalImageCapabilities.detect();
             if (caps.supportsNativeImages()) {
@@ -1336,7 +1434,9 @@ public class CamelMonitor extends CamelCommand {
                     layoutRoutes.add(lr);
                     totalHeight = lr.maxY;
                 }
-                RouteDiagramRenderer renderer = new RouteDiagramRenderer();
+                RouteDiagramRenderer renderer = new RouteDiagramRenderer(
+                        engine.getNodeWidth(), 
RouteDiagramLayoutEngine.DEFAULT_FONT_SIZE * RouteDiagramLayoutEngine.SCALE,
+                        metrics);
                 RouteDiagramRenderer.DiagramColors colors = 
RouteDiagramRenderer.DiagramColors.parse("transparent");
                 java.awt.image.BufferedImage image = 
renderer.renderDiagram(layoutRoutes, totalHeight, colors);
                 ImageData fullImage = ImageData.fromBufferedImage(image);
@@ -1353,44 +1453,35 @@ public class CamelMonitor extends CamelCommand {
 
     private void applyDiagramResult(
             String routeId, List<String> lines, ImageData imageData, ImageData 
fullImageData, ImageProtocol protocol) {
+        applyDiagramResult(routeId, lines, imageData, fullImageData, protocol, 
Collections.emptyList());
+    }
+
+    private void applyDiagramResult(
+            String routeId, List<String> lines, ImageData imageData, ImageData 
fullImageData, ImageProtocol protocol,
+            List<RouteDiagramAsciiRenderer.CounterPos> positions) {
         if (runner == null) {
             return;
         }
         runner.runOnRenderThread(() -> {
+            boolean wasShowing = showDiagram;
             diagramRouteId = routeId;
             diagramLines = lines;
+            diagramCounterPositions = positions;
             diagramImageData = imageData;
             diagramFullImageData = fullImageData;
             diagramProtocol = protocol;
-            diagramScroll = 0;
-            diagramScrollX = 0;
-            diagramCropX = -1;
-            diagramCropY = -1;
-            diagramCropW = -1;
-            diagramCropH = -1;
+            if (!wasShowing) {
+                diagramScroll = 0;
+                diagramScrollX = 0;
+                diagramCropX = -1;
+                diagramCropY = -1;
+                diagramCropW = -1;
+                diagramCropH = -1;
+            }
             showDiagram = true;
         });
     }
 
-    private static String renderAscii(
-            List<RouteDiagramLayoutEngine.RouteInfo> routes, int nodeWidth, 
String nodeLabel, boolean unicode) {
-        RouteDiagramLayoutEngine engine = new RouteDiagramLayoutEngine(
-                nodeWidth, RouteDiagramLayoutEngine.DEFAULT_FONT_SIZE,
-                
RouteDiagramLayoutEngine.NodeLabelMode.valueOf(nodeLabel.toUpperCase()));
-
-        List<RouteDiagramLayoutEngine.LayoutRoute> layoutRoutes = new 
ArrayList<>();
-        int currentY = RouteDiagramLayoutEngine.PADDING;
-        for (RouteDiagramLayoutEngine.RouteInfo route : routes) {
-            RouteDiagramLayoutEngine.LayoutRoute lr = 
engine.layoutRoute(route, currentY);
-            layoutRoutes.add(lr);
-            currentY = lr.maxY + RouteDiagramLayoutEngine.V_GAP;
-        }
-
-        RouteDiagramAsciiRenderer renderer = new RouteDiagramAsciiRenderer(
-                nodeWidth * RouteDiagramLayoutEngine.SCALE, unicode);
-        return renderer.renderDiagram(layoutRoutes, currentY);
-    }
-
     private static JsonObject pollJsonResponse(Path outputFile, long timeout) {
         long start = System.currentTimeMillis();
         while (System.currentTimeMillis() - start < timeout) {
@@ -1979,17 +2070,21 @@ public class CamelMonitor extends CamelCommand {
 
         if (tab == TAB_OVERVIEW) {
             hint(spans, "q", "quit");
-            hint(spans, "r", "refresh");
             hint(spans, "\u2191\u2193", "navigate");
             hint(spans, "Enter", "details");
             hint(spans, "1-6", "tabs");
-            hintRefresh(spans);
         } else if (tab == TAB_ROUTES && showDiagram) {
             String closeKey = diagramTextMode ? "D" : "d";
             hint(spans, closeKey + "/Esc", "close");
             hint(spans, "\u2191\u2193\u2190\u2192", "scroll");
             hint(spans, "PgUp/PgDn", "page");
-            hintLast(spans, "Home/End", "top/bottom");
+            hint(spans, "Home/End", "top/bottom");
+            if (diagramMetrics && !diagramTextMode) {
+                hint(spans, "m", "metrics [on]");
+                hintLast(spans, "F5", "refresh counters");
+            } else {
+                hintLast(spans, "m", "metrics" + (diagramMetrics ? " [on]" : " 
[off]"));
+            }
         } else if (tab == TAB_ROUTES) {
             hint(spans, "Esc", "back");
             hint(spans, "\u2191\u2193", "navigate");
@@ -1997,13 +2092,11 @@ public class CamelMonitor extends CamelCommand {
             hint(spans, "d", "diagram");
             hint(spans, "D", "text diagram");
             hint(spans, "1-6", "tabs");
-            hintRefresh(spans);
         } else if (tab == TAB_HEALTH) {
             hint(spans, "Esc", "back");
             hint(spans, "\u2191\u2193", "navigate");
             hint(spans, "d", "toggle DOWN");
             hint(spans, "1-6", "tabs");
-            hintRefresh(spans);
         } else if (tab == TAB_LOG) {
             hint(spans, "Esc", "back");
             hint(spans, "\u2191\u2193", "scroll");
@@ -2021,7 +2114,6 @@ public class CamelMonitor extends CamelCommand {
             hint(spans, "Esc", "back");
             hint(spans, "\u2191\u2193", "navigate");
             hint(spans, "1-6", "tabs");
-            hintRefresh(spans);
         }
 
         frame.renderWidget(Paragraph.from(Line.from(spans)), area);
@@ -2039,13 +2131,6 @@ public class CamelMonitor extends CamelCommand {
         spans.add(Span.raw(" " + label));
     }
 
-    private void hintRefresh(List<Span> spans) {
-        String refreshLabel = refreshInterval >= 1000
-                ? (refreshInterval / 1000) + "s"
-                : refreshInterval + "ms";
-        spans.add(Span.styled("Refresh: " + refreshLabel, Style.EMPTY.dim()));
-    }
-
     // ---- Data Loading ----
 
     private void refreshData() {


Reply via email to