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 68e4d6361ad3 CAMEL-23615: camel-jbang - TUI add Metrics tab with 
dashboard, table, and raw view (#23647)
68e4d6361ad3 is described below

commit 68e4d6361ad3bb96d9ed77061a19a301e51be79b
Author: Claus Ibsen <[email protected]>
AuthorDate: Fri May 29 20:59:32 2026 +0200

    CAMEL-23615: camel-jbang - TUI add Metrics tab with dashboard, table, and 
raw view (#23647)
    
    * CAMEL-23615: camel-jbang - TUI add Metrics tab with dashboard, table, and 
raw view
    
    Co-Authored-By: Claude <[email protected]>
    
    * CAMEL-23615: camel-jbang - TUI add Startup Timeline tab
    
    Co-Authored-By: Claude <[email protected]>
    
    * CAMEL-23615: camel-jbang - TUI add Configuration tab
    
    Co-Authored-By: Claude <[email protected]>
    
    * CAMEL-23615: camel-jbang - TUI fix More popup to overlay current tab and 
remember selection
    
    Co-Authored-By: Claude <[email protected]>
    
    ---------
    
    Co-authored-by: Claude <[email protected]>
---
 .../dsl/jbang/core/commands/tui/CamelMonitor.java  | 258 +++++-
 .../jbang/core/commands/tui/ConfigurationTab.java  | 240 ++++++
 .../jbang/core/commands/tui/IntegrationInfo.java   |   2 +
 .../dsl/jbang/core/commands/tui/MetricsTab.java    | 890 +++++++++++++++++++++
 .../core/commands/tui/MicrometerMeterInfo.java     |  41 +
 .../dsl/jbang/core/commands/tui/StartupTab.java    | 363 +++++++++
 6 files changed, 1765 insertions(+), 29 deletions(-)

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 f728deed7dd6..b51bb8b27a89 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
@@ -73,6 +73,10 @@ import dev.tamboui.widgets.barchart.BarGroup;
 import dev.tamboui.widgets.block.Block;
 import dev.tamboui.widgets.block.BorderType;
 import dev.tamboui.widgets.block.Title;
+import dev.tamboui.widgets.list.ListItem;
+import dev.tamboui.widgets.list.ListState;
+import dev.tamboui.widgets.list.ListWidget;
+import dev.tamboui.widgets.list.ScrollMode;
 import dev.tamboui.widgets.paragraph.Paragraph;
 import dev.tamboui.widgets.table.Cell;
 import dev.tamboui.widgets.table.Row;
@@ -119,8 +123,8 @@ public class CamelMonitor extends CamelCommand {
     private static final int TAB_HEALTH = 5;
     private static final int TAB_HISTORY = 6;
     private static final int TAB_ERRORS = 7;
-    private static final int TAB_CIRCUIT_BREAKER = 8;
-    private static final int TAB_CONSUMERS = 9;
+    private static final int TAB_METRICS = 8;
+    private static final int TAB_MORE = 9;
 
     // Overview sort columns
     private static final String[] OVERVIEW_SORT_COLUMNS = { "pid", "name", 
"version", "status", "total", "fail" };
@@ -271,6 +275,16 @@ public class CamelMonitor extends CamelCommand {
     private HistoryTab historyTab;
     private CircuitBreakerTab circuitBreakerTab;
     private ErrorsTab errorsTab;
+    private MetricsTab metricsTab;
+    private StartupTab startupTab;
+    private ConfigurationTab configurationTab;
+
+    // "More" dropdown state
+    private boolean showMorePopup;
+    private final ListState morePopupState = new ListState();
+    private MonitorTab activeMoreTab;
+    private int lastMoreSelection;
+    private Line[] currentTabLabels;
 
     private ClassLoader classLoader;
 
@@ -320,6 +334,9 @@ public class CamelMonitor extends CamelCommand {
         historyTab = new HistoryTab(ctx, traces, traceFilePositions);
         circuitBreakerTab = new CircuitBreakerTab(ctx, cbSuccessHistory, 
cbFailHistory);
         errorsTab = new ErrorsTab(ctx);
+        metricsTab = new MetricsTab(ctx);
+        startupTab = new StartupTab(ctx);
+        configurationTab = new ConfigurationTab(ctx);
 
         // Initial data load (synchronous before TUI starts)
         refreshDataSync();
@@ -402,6 +419,42 @@ public class CamelMonitor extends CamelCommand {
             if (actionsPopup.isVisible()) {
                 return actionsPopup.handleKeyEvent(ke);
             }
+            // "More" tab popup
+            if (showMorePopup) {
+                if (ke.isCancel()) {
+                    showMorePopup = false;
+                    return true;
+                }
+                if (ke.isUp()) {
+                    morePopupState.selectPrevious();
+                    return true;
+                }
+                if (ke.isDown()) {
+                    morePopupState.selectNext(4);
+                    return true;
+                }
+                if (ke.isConfirm()) {
+                    showMorePopup = false;
+                    Integer sel = morePopupState.selected();
+                    if (sel != null) {
+                        lastMoreSelection = sel;
+                        activeMoreTab = switch (sel) {
+                            case 0 -> circuitBreakerTab;
+                            case 1 -> configurationTab;
+                            case 2 -> consumersTab;
+                            case 3 -> startupTab;
+                            default -> null;
+                        };
+                        if (activeMoreTab != null) {
+                            selectCurrentIntegration();
+                            tabsState.select(TAB_MORE);
+                            activeMoreTab.onTabSelected();
+                        }
+                    }
+                    return true;
+                }
+                return true;
+            }
             // Kill confirm dialog: Enter to confirm, Esc/any other key to 
cancel
             if (showKillConfirm) {
                 if (ke.isConfirm()) {
@@ -463,10 +516,10 @@ public class CamelMonitor extends CamelCommand {
                     return handleTabKey(TAB_ERRORS);
                 }
                 if (ke.isChar('9')) {
-                    return handleTabKey(TAB_CIRCUIT_BREAKER);
+                    return handleTabKey(TAB_METRICS);
                 }
                 if (ke.isChar('0')) {
-                    return handleTabKey(TAB_CONSUMERS);
+                    return handleTabKey(TAB_MORE);
                 }
             }
 
@@ -733,9 +786,6 @@ public class CamelMonitor extends CamelCommand {
             }
             historyTab.onTabSelected();
         }
-        if (tab == TAB_CIRCUIT_BREAKER) {
-            circuitBreakerTab.onTabSelected();
-        }
         if (tab == TAB_ERRORS && ctx.selectedPid != null) {
             try {
                 long pid = Long.parseLong(ctx.selectedPid);
@@ -745,6 +795,14 @@ public class CamelMonitor extends CamelCommand {
             }
             errorsTab.onTabSelected();
         }
+        if (tab == TAB_MORE) {
+            showMorePopup = !showMorePopup;
+            if (showMorePopup) {
+                morePopupState.select(lastMoreSelection);
+            }
+            return true;
+        }
+        showMorePopup = false;
         tabsState.select(tab);
         return true;
     }
@@ -968,7 +1026,6 @@ public class CamelMonitor extends CamelCommand {
         IntegrationInfo sel = findSelectedIntegration();
         boolean hasSelection = ctx.selectedPid != null && sel != null;
         int routeCount = hasSelection ? sel.routes.size() : 0;
-        int consumerCount = hasSelection ? sel.consumers.size() : 0;
         int endpointCount = hasSelection ? sel.endpoints.size() : 0;
         int cbCount = hasSelection ? sel.circuitBreakers.size() : 0;
         long cbOpenCount = hasSelection
@@ -984,6 +1041,8 @@ public class CamelMonitor extends CamelCommand {
         boolean hasTraces = hasSelection && !traces.get().isEmpty();
         int httpCount = hasSelection ? sel.httpEndpoints.size() : 0;
 
+        int metricsCount = hasSelection ? sel.meters.size() : 0;
+
         // Row 0: label-only titles — fixed width so the tab bar never shifts 
when badges appear
         Line[] labels = {
                 Line.from(" 1 Overview "),
@@ -994,9 +1053,10 @@ public class CamelMonitor extends CamelCommand {
                 Line.from(" 6 Health "),
                 Line.from(" 7 Inspect "),
                 Line.from(" 8 Errors "),
-                Line.from(" 9 Circuit Breaker "),
-                Line.from(" 0 Consumer "),
+                Line.from(" 9 Metrics "),
+                Line.from(" 0 More▾ "),
         };
+        currentTabLabels = labels;
 
         Tabs tabs = Tabs.builder()
                 .titles(labels)
@@ -1030,9 +1090,6 @@ public class CamelMonitor extends CamelCommand {
             if (routeCount > 0) {
                 badgeTexts[TAB_ROUTES] = "(" + routeCount + ")";
             }
-            if (consumerCount > 0) {
-                badgeTexts[TAB_CONSUMERS] = "(" + consumerCount + ")";
-            }
             if (endpointCount > 0) {
                 badgeTexts[TAB_ENDPOINTS] = "(" + endpointCount + ")";
             }
@@ -1051,11 +1108,12 @@ public class CamelMonitor extends CamelCommand {
             } else if (historyCount > 0) {
                 badgeTexts[TAB_HISTORY] = "(" + historyCount + ")";
             }
+            if (metricsCount > 0) {
+                badgeTexts[TAB_METRICS] = "(" + metricsCount + ")";
+            }
             if (cbOpenCount > 0) {
-                badgeTexts[TAB_CIRCUIT_BREAKER] = "(" + cbOpenCount + " OPEN)";
-                badgeStyles[TAB_CIRCUIT_BREAKER] = red;
-            } else if (cbCount > 0) {
-                badgeTexts[TAB_CIRCUIT_BREAKER] = "(" + cbCount + ")";
+                badgeTexts[TAB_MORE] = "(" + cbOpenCount + " OPEN)";
+                badgeStyles[TAB_MORE] = red;
             }
             int errorCount = hasSelection ? sel.errorCount : 0;
             if (errorCount > 0) {
@@ -1090,19 +1148,64 @@ public class CamelMonitor extends CamelCommand {
         } else {
             renderOverview(frame, area);
         }
+        // Render "More" popup overlay when visible
+        if (showMorePopup) {
+            renderMorePopup(frame, area);
+        }
+    }
+
+    private void renderMorePopup(Frame frame, Rect area) {
+        int popupW = 22;
+        int popupH = 6;
+        // Position just below the "0 More▾" tab label
+        int dividerW = CharWidth.of(" | ");
+        int tabBarX = 0;
+        Line[] tabLabels = currentTabLabels;
+        if (tabLabels != null) {
+            for (int i = 0; i < tabLabels.length - 1; i++) {
+                tabBarX += tabLabels[i].width();
+                tabBarX += dividerW;
+            }
+        }
+        int x = area.left() + tabBarX;
+        int y = area.top();
+        if (x + popupW > area.right()) {
+            x = Math.max(area.left(), area.right() - popupW);
+        }
+        Rect popup = new Rect(x, y, Math.min(popupW, area.width() - (x - 
area.left())), Math.min(popupH, area.height()));
+
+        frame.renderWidget(Clear.INSTANCE, popup);
+
+        ListItem[] items = {
+                ListItem.from("  Circuit Breaker"),
+                ListItem.from("  Configuration"),
+                ListItem.from("  Consumers"),
+                ListItem.from("  Startup"),
+        };
+        ListWidget list = ListWidget.builder()
+                .items(items)
+                .highlightStyle(Style.EMPTY.fg(Color.WHITE).bold().onBlue())
+                .highlightSymbol("")
+                .scrollMode(ScrollMode.NONE)
+                .block(Block.builder()
+                        .borderType(BorderType.ROUNDED)
+                        .title(" More Tabs ")
+                        .build())
+                .build();
+        frame.renderStatefulWidget(list, popup, morePopupState);
     }
 
     private MonitorTab activeTab() {
         return switch (tabsState.selected()) {
             case TAB_LOG -> logTab;
             case TAB_ROUTES -> routesTab;
-            case TAB_CONSUMERS -> consumersTab;
             case TAB_ENDPOINTS -> endpointsTab;
-            case TAB_CIRCUIT_BREAKER -> circuitBreakerTab;
             case TAB_HEALTH -> healthTab;
             case TAB_HISTORY -> historyTab;
             case TAB_HTTP -> httpTab;
             case TAB_ERRORS -> errorsTab;
+            case TAB_METRICS -> metricsTab;
+            case TAB_MORE -> activeMoreTab;
             default -> null;
         };
     }
@@ -1874,17 +1977,23 @@ public class CamelMonitor extends CamelCommand {
             return;
         }
 
-        MonitorTab tab = activeTab();
-
-        if (tab != null) {
-            tab.renderFooter(spans);
-            // Insert F2 after the first hint (Esc) — each hint is 2 spans 
(key + label)
-            int insertPos = Math.min(2, spans.size());
-            List<Span> f2Spans = new ArrayList<>();
-            hint(f2Spans, "F2", "actions");
-            spans.addAll(insertPos, f2Spans);
+        if (showMorePopup) {
+            hint(spans, "Up/Down", "select");
+            hint(spans, "Enter", "open");
+            hint(spans, "Esc", "close");
         } else {
-            renderOverviewFooter(spans);
+            MonitorTab tab = activeTab();
+
+            if (tab != null) {
+                tab.renderFooter(spans);
+                // Insert F2 after the first hint (Esc) — each hint is 2 spans 
(key + label)
+                int insertPos = Math.min(2, spans.size());
+                List<Span> f2Spans = new ArrayList<>();
+                hint(f2Spans, "F2", "actions");
+                spans.addAll(insertPos, f2Spans);
+            } else {
+                renderOverviewFooter(spans);
+            }
         }
 
         List<Span> rightSpans = new ArrayList<>();
@@ -3178,6 +3287,16 @@ public class CamelMonitor extends CamelCommand {
             info.errorCount = errorsObj.getIntegerOrDefault("size", 0);
         }
 
+        // Parse micrometer metrics (optional, only present when --observe is 
used)
+        JsonObject micrometerObj = (JsonObject) root.get("micrometer");
+        if (micrometerObj != null) {
+            parseMicrometerMeters(micrometerObj, "counters", "counter", info);
+            parseMicrometerMeters(micrometerObj, "gauges", "gauge", info);
+            parseMicrometerMeters(micrometerObj, "timers", "timer", info);
+            parseMicrometerMeters(micrometerObj, "longTaskTimers", 
"longTaskTimer", info);
+            parseMicrometerMeters(micrometerObj, "distribution", 
"distribution", info);
+        }
+
         // Parse REST DSL services
         JsonObject restsObj = (JsonObject) root.get("rests");
         if (restsObj != null) {
@@ -3222,6 +3341,28 @@ public class CamelMonitor extends CamelCommand {
             parseHttpEndpoints(phpObj, "managementEndpoints", true, info);
         }
 
+        // Parse configuration properties (from PropertiesDevConsole)
+        JsonObject propsObj = (JsonObject) root.get("properties");
+        if (propsObj != null) {
+            JsonArray propArr = (JsonArray) propsObj.get("properties");
+            if (propArr != null) {
+                for (Object p : propArr) {
+                    JsonObject pj = (JsonObject) p;
+                    String key = pj.getString("key");
+                    if (key != null && !key.startsWith("camel.jbang.")) {
+                        ConfigurationTab.ConfigProperty cp = new 
ConfigurationTab.ConfigProperty();
+                        cp.key = key;
+                        cp.value = objToString(pj.get("value"));
+                        cp.defaultValue = pj.getString("defaultValue");
+                        cp.source = pj.getString("source");
+                        cp.location = pj.getString("location");
+                        info.configProperties.add(cp);
+                    }
+                }
+                
info.configProperties.sort(ConfigurationTab::compareCamelFirst);
+            }
+        }
+
         return info;
     }
 
@@ -3289,6 +3430,65 @@ public class CamelMonitor extends CamelCommand {
         }
     }
 
+    @SuppressWarnings("unchecked")
+    private static void parseMicrometerMeters(
+            JsonObject micrometerObj, String section, String type, 
IntegrationInfo info) {
+        JsonArray arr = (JsonArray) micrometerObj.get(section);
+        if (arr == null) {
+            return;
+        }
+        for (Object o : arr) {
+            JsonObject jo = (JsonObject) o;
+            MicrometerMeterInfo m = new MicrometerMeterInfo();
+            m.type = type;
+            m.name = jo.getString("name");
+            m.description = jo.getString("description");
+            // parse tags
+            JsonArray tagsArr = (JsonArray) jo.get("tags");
+            if (tagsArr != null) {
+                for (Object t : tagsArr) {
+                    JsonObject tj = (JsonObject) t;
+                    m.tags.add(new String[] { tj.getString("key"), 
tj.getString("value") });
+                }
+            }
+            // parse type-specific values
+            switch (type) {
+                case "counter":
+                    m.count = TuiHelper.objToLong(jo.get("count"));
+                    break;
+                case "gauge":
+                    Object v = jo.get("value");
+                    m.value = v instanceof Number n ? n.doubleValue() : null;
+                    break;
+                case "timer":
+                    m.count = TuiHelper.objToLong(jo.get("count"));
+                    m.mean = TuiHelper.objToLong(jo.get("mean"));
+                    m.max = TuiHelper.objToLong(jo.get("max"));
+                    m.total = TuiHelper.objToLong(jo.get("total"));
+                    break;
+                case "longTaskTimer":
+                    Object at = jo.get("activeTasks");
+                    m.activeTasks = at instanceof Number n ? n.intValue() : 
null;
+                    m.mean = TuiHelper.objToLong(jo.get("mean"));
+                    m.max = TuiHelper.objToLong(jo.get("max"));
+                    m.total = TuiHelper.objToLong(jo.get("duration"));
+                    break;
+                case "distribution":
+                    m.count = TuiHelper.objToLong(jo.get("count"));
+                    Object dm = jo.get("mean");
+                    m.meanDouble = dm instanceof Number n ? n.doubleValue() : 
null;
+                    Object dx = jo.get("max");
+                    m.maxDouble = dx instanceof Number n ? n.doubleValue() : 
null;
+                    Object dt = jo.get("totalAmount");
+                    m.totalDouble = dt instanceof Number n ? n.doubleValue() : 
null;
+                    break;
+                default:
+                    break;
+            }
+            info.meters.add(m);
+        }
+    }
+
     @SuppressWarnings("unchecked")
     private static void parseKvArray(JsonArray arr, Map<String, Object> 
values, Map<String, String> types) {
         if (arr == null) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConfigurationTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConfigurationTab.java
new file mode 100644
index 000000000000..c55daaa49e9b
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConfigurationTab.java
@@ -0,0 +1,240 @@
+/*
+ * 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.List;
+
+import dev.tamboui.layout.Constraint;
+import dev.tamboui.layout.Layout;
+import dev.tamboui.layout.Rect;
+import dev.tamboui.style.Color;
+import dev.tamboui.style.Style;
+import dev.tamboui.terminal.Frame;
+import dev.tamboui.text.Line;
+import dev.tamboui.text.Span;
+import dev.tamboui.text.Text;
+import dev.tamboui.tui.event.KeyCode;
+import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.widgets.block.Block;
+import dev.tamboui.widgets.block.BorderType;
+import dev.tamboui.widgets.paragraph.Paragraph;
+import dev.tamboui.widgets.scrollbar.Scrollbar;
+import dev.tamboui.widgets.scrollbar.ScrollbarState;
+
+import static org.apache.camel.dsl.jbang.core.commands.tui.MonitorContext.*;
+
+class ConfigurationTab implements MonitorTab {
+
+    private static final Style KEY_STYLE = Style.EMPTY.fg(Color.CYAN);
+    private static final Style VALUE_STYLE = Style.EMPTY.fg(Color.WHITE);
+    private static final Style SECRET_STYLE = Style.EMPTY.fg(Color.DARK_GRAY);
+    private static final Style SOURCE_STYLE = Style.EMPTY.dim();
+
+    private final MonitorContext ctx;
+    private final ScrollbarState scrollbarState = new ScrollbarState();
+    private int scrollOffset;
+
+    ConfigurationTab(MonitorContext ctx) {
+        this.ctx = ctx;
+    }
+
+    @Override
+    public boolean handleKeyEvent(KeyEvent ke) {
+        if (ke.isUp()) {
+            scrollOffset = Math.max(0, scrollOffset - 1);
+            return true;
+        }
+        if (ke.isDown()) {
+            scrollOffset++;
+            return true;
+        }
+        if (ke.isPageUp() || ke.isKey(KeyCode.PAGE_UP)) {
+            scrollOffset = Math.max(0, scrollOffset - 20);
+            return true;
+        }
+        if (ke.isPageDown() || ke.isKey(KeyCode.PAGE_DOWN)) {
+            scrollOffset += 20;
+            return true;
+        }
+        if (ke.isHome()) {
+            scrollOffset = 0;
+            return true;
+        }
+        if (ke.isEnd()) {
+            scrollOffset = Integer.MAX_VALUE;
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public boolean handleEscape() {
+        return false;
+    }
+
+    @Override
+    public void navigateUp() {
+        scrollOffset = Math.max(0, scrollOffset - 1);
+    }
+
+    @Override
+    public void navigateDown() {
+        scrollOffset++;
+    }
+
+    @Override
+    public void render(Frame frame, Rect area) {
+        IntegrationInfo info = ctx.findSelectedIntegration();
+        if (info == null) {
+            renderNoSelection(frame, area);
+            return;
+        }
+
+        List<ConfigProperty> props = info.configProperties;
+        if (props.isEmpty()) {
+            frame.renderWidget(
+                    Paragraph.builder()
+                            .text(Text.from(Line.from(
+                                    Span.styled("  No configuration properties 
available.", Style.EMPTY.dim()))))
+                            
.block(Block.builder().borderType(BorderType.ROUNDED)
+                                    .title(" Configuration ").build())
+                            .build(),
+                    area);
+            return;
+        }
+
+        // Find divider position: index of first non-camel property
+        int dividerIndex = -1;
+        for (int i = 0; i < props.size(); i++) {
+            if (!props.get(i).key.startsWith("camel.")) {
+                dividerIndex = i;
+                break;
+            }
+        }
+        boolean hasDivider = dividerIndex > 0 && dividerIndex < props.size();
+        int totalLines = props.size() + (hasDivider ? 1 : 0);
+
+        String title = String.format(" Configuration — %d properties ", 
props.size());
+        Block block = Block.builder()
+                .borderType(BorderType.ROUNDED)
+                .title(title)
+                .build();
+        Rect inner = block.inner(area);
+        frame.renderWidget(block, area);
+
+        if (inner.height() < 1 || inner.width() < 10) {
+            return;
+        }
+
+        int visibleLines = inner.height();
+        int maxScroll = Math.max(0, totalLines - visibleLines);
+        scrollOffset = Math.min(scrollOffset, maxScroll);
+
+        // Compute max key length across visible properties for alignment
+        int maxKeyLen = 0;
+        for (ConfigProperty p : props) {
+            maxKeyLen = Math.max(maxKeyLen, p.key.length());
+        }
+        int keyWidth = Math.min(maxKeyLen, inner.width() / 2);
+
+        // Build visible lines, inserting divider at the right display position
+        List<Line> lines = new ArrayList<>();
+        int displayRow = 0;
+        for (int i = 0; i < props.size() && lines.size() < visibleLines; i++) {
+            if (hasDivider && i == dividerIndex) {
+                if (displayRow >= scrollOffset) {
+                    String divText = "─".repeat(Math.max(1, inner.width() - 
2));
+                    lines.add(Line.from(Span.styled(" " + divText, 
Style.EMPTY.dim())));
+                }
+                displayRow++;
+                if (lines.size() >= visibleLines) {
+                    break;
+                }
+            }
+            if (displayRow >= scrollOffset) {
+                lines.add(renderProperty(props.get(i), keyWidth));
+            }
+            displayRow++;
+        }
+
+        List<Rect> hChunks = Layout.horizontal()
+                .constraints(Constraint.fill(), Constraint.length(1))
+                .split(inner);
+
+        frame.renderWidget(Paragraph.builder().text(Text.from(lines)).build(), 
hChunks.get(0));
+
+        if (totalLines > visibleLines) {
+            scrollbarState
+                    .contentLength(totalLines)
+                    .viewportContentLength(visibleLines)
+                    .position(scrollOffset);
+            frame.renderStatefulWidget(Scrollbar.builder().build(), 
hChunks.get(1), scrollbarState);
+        }
+    }
+
+    private Line renderProperty(ConfigProperty prop, int keyWidth) {
+        String key = prop.key;
+        if (key.length() > keyWidth) {
+            key = key.substring(0, keyWidth - 1) + "…";
+        } else {
+            key = String.format("%-" + keyWidth + "s", key);
+        }
+
+        boolean secret = "xxxxxx".equals(prop.value);
+        Style valStyle = secret ? SECRET_STYLE : VALUE_STYLE;
+        String value = prop.value != null ? prop.value : "";
+
+        List<Span> spans = new ArrayList<>();
+        spans.add(Span.styled("  " + key + "  ", KEY_STYLE));
+        spans.add(Span.styled(value, valStyle));
+        if (prop.source != null && !prop.source.isEmpty()) {
+            spans.add(Span.styled("  [" + prop.source + "]", SOURCE_STYLE));
+        }
+
+        return Line.from(spans);
+    }
+
+    @Override
+    public void renderFooter(List<Span> spans) {
+        hint(spans, "Esc", "back");
+        hint(spans, "↑↓", "scroll");
+        hintLast(spans, "PgUp/Dn", "page");
+    }
+
+    @Override
+    public SelectionContext getSelectionContext() {
+        return null;
+    }
+
+    static int compareCamelFirst(ConfigProperty a, ConfigProperty b) {
+        boolean aCamel = a.key.startsWith("camel.");
+        boolean bCamel = b.key.startsWith("camel.");
+        if (aCamel != bCamel) {
+            return aCamel ? -1 : 1;
+        }
+        return a.key.compareToIgnoreCase(b.key);
+    }
+
+    static class ConfigProperty {
+        String key;
+        String value;
+        String defaultValue;
+        String source;
+        String location;
+    }
+}
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 018fabc1a6f1..21bfc0e1b7ea 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
@@ -65,7 +65,9 @@ class IntegrationInfo {
     final List<CircuitBreakerInfo> circuitBreakers = new ArrayList<>();
     int errorCount;
     final List<ErrorInfo> errors = new ArrayList<>();
+    final List<MicrometerMeterInfo> meters = new ArrayList<>();
     final List<HttpEndpointInfo> httpEndpoints = new ArrayList<>();
+    final List<ConfigurationTab.ConfigProperty> configProperties = new 
ArrayList<>();
     String httpServer;
     String readmeFiles;
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MetricsTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MetricsTab.java
new file mode 100644
index 000000000000..c4b26037fc2c
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MetricsTab.java
@@ -0,0 +1,890 @@
+/*
+ * 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.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.stream.Collectors;
+
+import dev.tamboui.layout.Constraint;
+import dev.tamboui.layout.Layout;
+import dev.tamboui.layout.Rect;
+import dev.tamboui.style.Color;
+import dev.tamboui.style.Style;
+import dev.tamboui.terminal.Frame;
+import dev.tamboui.text.Line;
+import dev.tamboui.text.Span;
+import dev.tamboui.text.Text;
+import dev.tamboui.tui.event.KeyCode;
+import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.widgets.block.Block;
+import dev.tamboui.widgets.block.BorderType;
+import dev.tamboui.widgets.paragraph.Paragraph;
+import dev.tamboui.widgets.scrollbar.Scrollbar;
+import dev.tamboui.widgets.scrollbar.ScrollbarState;
+import dev.tamboui.widgets.table.Cell;
+import dev.tamboui.widgets.table.Row;
+import dev.tamboui.widgets.table.Table;
+import dev.tamboui.widgets.table.TableState;
+
+import static org.apache.camel.dsl.jbang.core.commands.tui.MonitorContext.*;
+
+class MetricsTab implements MonitorTab {
+
+    private static final String[] SORT_COLUMNS = { "name", "type", "value" };
+    private static final String[] FILTER_TYPES = { "all", "counter", "gauge", 
"timer", "longTaskTimer", "distribution" };
+
+    private static final Style LABEL = Style.EMPTY.dim();
+    private static final Style VALUE = Style.EMPTY.fg(Color.WHITE).bold();
+    private static final Style HEADER = Style.EMPTY.fg(Color.YELLOW).bold();
+    private static final Style GOOD = Style.EMPTY.fg(Color.GREEN);
+    private static final Style BAD = Style.EMPTY.fg(Color.LIGHT_RED);
+
+    private final MonitorContext ctx;
+    private final TableState tableState = new TableState();
+    private final ScrollbarState scrollbarState = new ScrollbarState();
+    private final ScrollbarState rawScrollbarState = new ScrollbarState();
+    private boolean tableMode;
+    private int lastRowCount;
+    private String sort = "name";
+    private int sortIndex;
+    private boolean sortReversed;
+    private String filterType = "all";
+    private int filterIndex;
+
+    // raw metrics view
+    private boolean showRaw;
+    private List<String> rawLines = Collections.emptyList();
+    private int rawScroll;
+    private String rawTitle;
+    private String rawContentType;
+    private final AtomicBoolean rawLoading = new AtomicBoolean(false);
+
+    MetricsTab(MonitorContext ctx) {
+        this.ctx = ctx;
+    }
+
+    @Override
+    public boolean handleKeyEvent(KeyEvent ke) {
+        // raw view scrolling
+        if (showRaw) {
+            if (ke.isUp()) {
+                rawScroll = Math.max(0, rawScroll - 1);
+                return true;
+            }
+            if (ke.isDown()) {
+                rawScroll++;
+                return true;
+            }
+            if (ke.isPageUp()) {
+                rawScroll = Math.max(0, rawScroll - 20);
+                return true;
+            }
+            if (ke.isPageDown()) {
+                rawScroll += 20;
+                return true;
+            }
+            if (ke.isHome()) {
+                rawScroll = 0;
+                return true;
+            }
+            if (ke.isEnd()) {
+                rawScroll = Integer.MAX_VALUE;
+                return true;
+            }
+            if (ke.isKey(KeyCode.F5)) {
+                loadRawMetrics();
+                return true;
+            }
+            return false;
+        }
+
+        if (ke.isChar('r')) {
+            IntegrationInfo info = ctx.findSelectedIntegration();
+            if (info != null && findMetricsUrl(info) != null) {
+                showRaw = true;
+                rawScroll = 0;
+                loadRawMetrics();
+                return true;
+            }
+        }
+        if (ke.isChar('d')) {
+            tableMode = !tableMode;
+            return true;
+        }
+        if (tableMode) {
+            if (ke.isPageUp()) {
+                for (int i = 0; i < 10; i++) {
+                    tableState.selectPrevious();
+                }
+                return true;
+            }
+            if (ke.isPageDown()) {
+                for (int i = 0; i < 10; i++) {
+                    tableState.selectNext(lastRowCount);
+                }
+                return true;
+            }
+            if (ke.isHome()) {
+                tableState.selectFirst();
+                return true;
+            }
+            if (ke.isEnd()) {
+                tableState.selectLast(lastRowCount);
+                return true;
+            }
+            if (ke.isChar('s')) {
+                sortIndex = (sortIndex + 1) % SORT_COLUMNS.length;
+                sort = SORT_COLUMNS[sortIndex];
+                sortReversed = false;
+                return true;
+            }
+            if (ke.isChar('S')) {
+                sortReversed = !sortReversed;
+                return true;
+            }
+            if (ke.isChar('f')) {
+                filterIndex = (filterIndex + 1) % FILTER_TYPES.length;
+                filterType = FILTER_TYPES[filterIndex];
+                return true;
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public boolean handleEscape() {
+        if (showRaw) {
+            showRaw = false;
+            return true;
+        }
+        if (tableMode) {
+            tableMode = false;
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public void navigateUp() {
+        if (tableMode) {
+            tableState.selectPrevious();
+        }
+    }
+
+    @Override
+    public void navigateDown() {
+        if (tableMode) {
+            tableState.selectNext(lastRowCount);
+        }
+    }
+
+    @Override
+    public void render(Frame frame, Rect area) {
+        IntegrationInfo info = ctx.findSelectedIntegration();
+        if (info == null) {
+            renderNoSelection(frame, area);
+            return;
+        }
+
+        if (info.meters.isEmpty()) {
+            Paragraph p = Paragraph.from(Line.from(
+                    Span.styled("No metrics available. Run with --observe to 
enable micrometer.", LABEL)));
+            frame.renderWidget(p, area);
+            return;
+        }
+
+        if (showRaw) {
+            renderRaw(frame, area);
+        } else if (tableMode) {
+            renderTable(frame, area, info);
+        } else {
+            renderDashboard(frame, area, info);
+        }
+    }
+
+    // ---- Dashboard mode ----
+
+    private void renderDashboard(Frame frame, Rect area, IntegrationInfo info) 
{
+        String metricsUrl = findMetricsUrl(info);
+        Rect panelArea = area;
+        if (metricsUrl != null) {
+            List<Rect> vParts = Layout.vertical()
+                    .constraints(Constraint.length(1), Constraint.fill())
+                    .split(area);
+            frame.renderWidget(Paragraph.from(Line.from(
+                    Span.styled("  Endpoint: ", LABEL),
+                    Span.styled(metricsUrl, Style.EMPTY.fg(Color.CYAN)))), 
vParts.get(0));
+            panelArea = vParts.get(1);
+        }
+
+        List<Rect> panels = Layout.horizontal()
+                .constraints(Constraint.percentage(50), 
Constraint.percentage(50))
+                .split(panelArea);
+
+        renderCamelPanel(frame, panels.get(0), info.meters);
+        renderJvmPanel(frame, panels.get(1), info.meters);
+    }
+
+    private void renderCamelPanel(Frame frame, Rect area, 
List<MicrometerMeterInfo> meters) {
+        List<Line> lines = new ArrayList<>();
+
+        // Exchanges section
+        lines.add(Line.from(Span.styled("  Exchanges", HEADER)));
+        lines.add(Line.empty());
+
+        long total = counterValue(meters, "camel.exchanges.total");
+        long failed = counterValue(meters, "camel.exchanges.failed");
+        long succeeded = counterValue(meters, "camel.exchanges.succeeded");
+        long inflight = gaugeValueLong(meters, "camel.exchanges.inflight");
+        long extReloaded = counterValue(meters, 
"camel.exchanges.external.reloaded");
+
+        lines.add(Line.from(
+                Span.styled("    Total:     ", LABEL),
+                Span.styled(formatNumber(total), VALUE),
+                Span.styled("    Inflight:  ", LABEL),
+                Span.styled(String.valueOf(inflight), inflight > 0 ? GOOD : 
VALUE)));
+        lines.add(Line.from(
+                Span.styled("    Succeeded: ", LABEL),
+                Span.styled(formatNumber(succeeded), GOOD),
+                Span.styled("    Failed:    ", LABEL),
+                Span.styled(formatNumber(failed), failed > 0 ? BAD : VALUE)));
+        if (extReloaded > 0) {
+            lines.add(Line.from(
+                    Span.styled("    Ext Reloaded: ", LABEL),
+                    Span.styled(formatNumber(extReloaded), VALUE)));
+        }
+        lines.add(Line.empty());
+
+        // Route timers
+        List<MicrometerMeterInfo> routeTimers = findMeters(meters, 
"camel.route.policy");
+        if (!routeTimers.isEmpty()) {
+            lines.add(Line.from(Span.styled("  Route Timers", HEADER),
+                    Span.styled("                     mean / max", LABEL)));
+            lines.add(Line.empty());
+            for (MicrometerMeterInfo rt : routeTimers) {
+                String routeId = tagValue(rt, "routeId");
+                if (routeId == null) {
+                    routeId = tagValue(rt, "routeid");
+                }
+                if (routeId == null) {
+                    routeId = "?";
+                }
+                String timing = String.format("%dms / %dms",
+                        rt.mean != null ? rt.mean : 0,
+                        rt.max != null ? rt.max : 0);
+                int pad = Math.max(1, 30 - routeId.length());
+                lines.add(Line.from(
+                        Span.styled("    " + routeId, 
Style.EMPTY.fg(Color.CYAN)),
+                        Span.styled(" ".repeat(pad), Style.EMPTY),
+                        Span.styled(timing, VALUE)));
+            }
+        }
+
+        // Exchange event notifier timers
+        List<MicrometerMeterInfo> eventTimers = findMeters(meters, 
"camel.exchange.event.notifier");
+        if (!eventTimers.isEmpty()) {
+            lines.add(Line.empty());
+            lines.add(Line.from(Span.styled("  Event Notifiers", HEADER),
+                    Span.styled("                  mean / max", LABEL)));
+            lines.add(Line.empty());
+            for (MicrometerMeterInfo et : eventTimers) {
+                String name = et.name != null ? et.name : "?";
+                String shortName = 
name.replace("camel.exchange.event.notifier.", "");
+                String timing = String.format("%dms / %dms",
+                        et.mean != null ? et.mean : 0,
+                        et.max != null ? et.max : 0);
+                int pad = Math.max(1, 30 - shortName.length());
+                lines.add(Line.from(
+                        Span.styled("    " + shortName, 
Style.EMPTY.fg(Color.CYAN)),
+                        Span.styled(" ".repeat(pad), Style.EMPTY),
+                        Span.styled(timing, VALUE)));
+            }
+        }
+
+        Paragraph paragraph = Paragraph.builder()
+                .text(Text.from(lines))
+                .block(Block.builder().borderType(BorderType.ROUNDED).title(" 
Camel ").build())
+                .build();
+        frame.renderWidget(paragraph, area);
+    }
+
+    private void renderJvmPanel(Frame frame, Rect area, 
List<MicrometerMeterInfo> meters) {
+        List<Line> lines = new ArrayList<>();
+
+        // Memory section
+        lines.add(Line.from(Span.styled("  Memory", HEADER)));
+        lines.add(Line.empty());
+
+        double heapUsed = gaugeValue(meters, "jvm.memory.used", "area", 
"heap");
+        double heapMax = gaugeValue(meters, "jvm.memory.max", "area", "heap");
+        double heapCommitted = gaugeValue(meters, "jvm.memory.committed", 
"area", "heap");
+        double nonHeapUsed = gaugeValue(meters, "jvm.memory.used", "area", 
"nonheap");
+
+        String heapStr = formatBytes(heapUsed);
+        String heapMaxStr = heapMax > 0 ? formatBytes(heapMax) : "-";
+        String memBar = heapMax > 0 ? memoryBar(heapUsed, heapMax, 10) : "";
+
+        lines.add(Line.from(
+                Span.styled("    Heap:       ", LABEL),
+                Span.styled(heapStr, VALUE),
+                Span.styled(" / ", LABEL),
+                Span.styled(heapMaxStr, VALUE),
+                Span.styled("  ", Style.EMPTY),
+                Span.styled(memBar, heapMax > 0 && heapUsed / heapMax > 0.85 ? 
BAD : GOOD)));
+
+        if (heapCommitted > 0) {
+            lines.add(Line.from(
+                    Span.styled("    Committed:  ", LABEL),
+                    Span.styled(formatBytes(heapCommitted), VALUE)));
+        }
+        lines.add(Line.from(
+                Span.styled("    Non-heap:   ", LABEL),
+                Span.styled(formatBytes(nonHeapUsed), VALUE)));
+        lines.add(Line.empty());
+
+        // Runtime section
+        lines.add(Line.from(Span.styled("  Runtime", HEADER)));
+        lines.add(Line.empty());
+
+        double cpuProcess = gaugeValue(meters, "process.cpu.usage");
+        double cpuSystem = gaugeValue(meters, "system.cpu.usage");
+        double threads = gaugeValue(meters, "jvm.threads.live");
+        if (threads == 0) {
+            threads = gaugeValue(meters, "jvm.threads.current");
+        }
+        double peakThreads = gaugeValue(meters, "jvm.threads.peak");
+        double daemonThreads = gaugeValue(meters, "jvm.threads.daemon");
+
+        if (cpuProcess >= 0) {
+            lines.add(Line.from(
+                    Span.styled("    CPU (proc): ", LABEL),
+                    Span.styled(String.format("%.1f%%", cpuProcess * 100), 
VALUE)));
+        }
+        if (cpuSystem >= 0) {
+            lines.add(Line.from(
+                    Span.styled("    CPU (sys):  ", LABEL),
+                    Span.styled(String.format("%.1f%%", cpuSystem * 100), 
VALUE)));
+        }
+        if (threads > 0) {
+            lines.add(Line.from(
+                    Span.styled("    Threads:    ", LABEL),
+                    Span.styled(String.valueOf((long) threads), VALUE),
+                    peakThreads > 0
+                            ? Span.styled("  (peak: " + (long) peakThreads + 
")", LABEL)
+                            : Span.raw("")));
+        }
+        if (daemonThreads > 0) {
+            lines.add(Line.from(
+                    Span.styled("    Daemon:     ", LABEL),
+                    Span.styled(String.valueOf((long) daemonThreads), VALUE)));
+        }
+
+        // GC
+        long gcCount = counterValue(meters, "jvm.gc.pause");
+        List<MicrometerMeterInfo> gcTimers = findMeters(meters, 
"jvm.gc.pause");
+        if (!gcTimers.isEmpty()) {
+            lines.add(Line.empty());
+            lines.add(Line.from(Span.styled("  Garbage Collection", HEADER)));
+            lines.add(Line.empty());
+            for (MicrometerMeterInfo gc : gcTimers) {
+                String cause = tagValue(gc, "cause");
+                String action = tagValue(gc, "action");
+                String label = cause != null ? cause : (action != null ? 
action : "GC");
+                lines.add(Line.from(
+                        Span.styled("    " + label + ": ", LABEL),
+                        Span.styled("count=", LABEL),
+                        Span.styled(String.valueOf(gc.count != null ? gc.count 
: 0), VALUE),
+                        Span.styled(", total=", LABEL),
+                        Span.styled((gc.total != null ? gc.total : 0) + "ms", 
VALUE)));
+            }
+        }
+
+        Paragraph paragraph = Paragraph.builder()
+                .text(Text.from(lines))
+                .block(Block.builder().borderType(BorderType.ROUNDED).title(" 
JVM ").build())
+                .build();
+        frame.renderWidget(paragraph, area);
+    }
+
+    // ---- Table mode ----
+
+    private void renderTable(Frame frame, Rect area, IntegrationInfo info) {
+        List<MicrometerMeterInfo> filtered = info.meters;
+        if (!"all".equals(filterType)) {
+            filtered = filtered.stream()
+                    .filter(m -> filterType.equals(m.type))
+                    .collect(Collectors.toList());
+        }
+
+        List<MicrometerMeterInfo> sorted = new ArrayList<>(filtered);
+        sorted.sort(this::sortMeter);
+
+        List<Row> rows = new ArrayList<>();
+        for (MicrometerMeterInfo m : sorted) {
+            String typeLabel = typeLabel(m.type);
+            Style typeStyle = typeStyle(m.type);
+            String value = formatValue(m);
+            String tags = formatTags(m);
+
+            rows.add(Row.from(
+                    Cell.from(Span.styled(typeLabel, typeStyle)),
+                    Cell.from(Span.styled(m.name != null ? m.name : "", 
Style.EMPTY.fg(Color.CYAN))),
+                    Cell.from(value),
+                    Cell.from(Span.styled(tags, Style.EMPTY.dim()))));
+        }
+
+        lastRowCount = rows.size();
+        if (!rows.isEmpty() && tableState.selected() == null) {
+            tableState.select(0);
+        }
+
+        if (rows.isEmpty()) {
+            String msg = "No " + filterType + " metrics";
+            rows.add(Row.from(
+                    Cell.from(Span.styled(msg, Style.EMPTY.dim())),
+                    Cell.from(""), Cell.from(""), Cell.from("")));
+        }
+
+        String title = " Metrics";
+        if (!"all".equals(filterType)) {
+            title += " filter:" + filterType;
+        }
+        title += " sort:" + sort + " (" + sorted.size() + ") ";
+
+        Table table = Table.builder()
+                .rows(rows)
+                .header(Row.from(
+                        Cell.from(Span.styled(sortLabel("TYPE", "type"), 
sortStyle("type"))),
+                        Cell.from(Span.styled(sortLabel("NAME", "name"), 
sortStyle("name"))),
+                        Cell.from(Span.styled(sortLabel("VALUE", "value"), 
sortStyle("value"))),
+                        Cell.from(Span.styled("TAGS", Style.EMPTY.bold()))))
+                .widths(
+                        Constraint.length(12),
+                        Constraint.length(40),
+                        Constraint.percentage(30),
+                        Constraint.fill())
+                .highlightStyle(Style.EMPTY.fg(Color.WHITE).bold().onBlue())
+                .block(Block.builder().borderType(BorderType.ROUNDED)
+                        .title(title).build())
+                .build();
+
+        frame.renderStatefulWidget(table, area, tableState);
+
+        int visibleRows = Math.max(1, area.height() - 4);
+        if (lastRowCount > visibleRows) {
+            Integer sel = tableState.selected();
+            scrollbarState
+                    .contentLength(lastRowCount)
+                    .viewportContentLength(visibleRows)
+                    .position(sel != null ? sel : 0);
+            frame.renderStatefulWidget(Scrollbar.builder().build(), area, 
scrollbarState);
+        }
+    }
+
+    // ---- Raw metrics view ----
+
+    private void renderRaw(Frame frame, Rect area) {
+        String ct = rawContentType != null ? " " + rawContentType : "";
+        String title = " Raw Metrics (" + rawLines.size() + " lines)" + ct + " 
[" + (rawTitle != null ? rawTitle : "") + "] ";
+
+        Block block = Block.builder()
+                .borderType(BorderType.ROUNDED)
+                .title(title)
+                .build();
+        Rect inner = block.inner(area);
+        frame.renderWidget(block, area);
+
+        if (rawLines.isEmpty()) {
+            return;
+        }
+
+        int visibleLines = inner.height();
+        int maxScroll = Math.max(0, rawLines.size() - visibleLines);
+        rawScroll = Math.min(rawScroll, maxScroll);
+
+        int end = Math.min(rawScroll + visibleLines, rawLines.size());
+        List<Line> visible = new ArrayList<>();
+        for (int i = rawScroll; i < end; i++) {
+            visible.add(colorPrometheusLine(rawLines.get(i)));
+        }
+
+        List<Rect> hChunks = Layout.horizontal()
+                .constraints(Constraint.fill(), Constraint.length(1))
+                .split(inner);
+
+        
frame.renderWidget(Paragraph.builder().text(Text.from(visible)).build(), 
hChunks.get(0));
+
+        if (rawLines.size() > visibleLines) {
+            rawScrollbarState
+                    .contentLength(rawLines.size())
+                    .viewportContentLength(visibleLines)
+                    .position(rawScroll);
+            frame.renderStatefulWidget(Scrollbar.builder().build(), 
hChunks.get(1), rawScrollbarState);
+        }
+    }
+
+    private void loadRawMetrics() {
+        IntegrationInfo info = ctx.findSelectedIntegration();
+        if (info == null) {
+            return;
+        }
+        String url = findMetricsUrl(info);
+        if (url == null) {
+            return;
+        }
+        if (!rawLoading.compareAndSet(false, true)) {
+            return;
+        }
+        rawTitle = url;
+
+        ctx.runner.scheduler().execute(() -> {
+            try {
+                HttpClient client = HttpClient.newBuilder()
+                        .connectTimeout(Duration.ofSeconds(5))
+                        .build();
+                HttpRequest request = HttpRequest.newBuilder()
+                        .uri(URI.create(url))
+                        .timeout(Duration.ofSeconds(10))
+                        .GET()
+                        .build();
+                HttpResponse<String> response = client.send(request, 
HttpResponse.BodyHandlers.ofString());
+                String ct = 
response.headers().firstValue("Content-Type").orElse(null);
+                List<String> lines = List.of(response.body().split("\n", -1));
+                applyRawResult(url, lines, ct);
+            } catch (Exception e) {
+                applyRawResult(url, List.of("(Error fetching metrics: " + 
e.getMessage() + ")"), null);
+            } finally {
+                rawLoading.set(false);
+            }
+        });
+    }
+
+    private void applyRawResult(String url, List<String> lines, String 
contentType) {
+        if (ctx.runner == null) {
+            return;
+        }
+        ctx.runner.runOnRenderThread(() -> {
+            if (!showRaw) {
+                return;
+            }
+            rawTitle = url;
+            rawLines = lines;
+            rawContentType = contentType;
+        });
+    }
+
+    @Override
+    public void renderFooter(List<Span> spans) {
+        if (showRaw) {
+            hint(spans, "↑↓", "scroll");
+            hint(spans, "PgUp/Dn", "page");
+            hint(spans, "F5", "refresh");
+            hintLast(spans, "Esc", "close");
+            return;
+        }
+        hint(spans, "Esc", "back");
+        hint(spans, "d", tableMode ? "dashboard" : "table");
+        if (tableMode) {
+            hint(spans, "s", "sort");
+            hint(spans, "f", "filter:" + filterType);
+        }
+        IntegrationInfo info = ctx.findSelectedIntegration();
+        if (info != null && findMetricsUrl(info) != null) {
+            hint(spans, "r", "raw");
+        }
+        hint(spans, "1-9", "tabs");
+    }
+
+    // ---- Prometheus syntax coloring ----
+
+    private static final Style PROM_COMMENT = Style.EMPTY.dim();
+    private static final Style PROM_NAME = Style.EMPTY.fg(Color.CYAN);
+    private static final Style PROM_VALUE = Style.EMPTY.fg(Color.WHITE).bold();
+    private static final Style PROM_TAGS = Style.EMPTY.fg(Color.YELLOW);
+
+    private static Line colorPrometheusLine(String line) {
+        if (line.startsWith("#")) {
+            return Line.from(Span.styled(line, PROM_COMMENT));
+        }
+        // metric line: name{tags} value  or  name value
+        int braceStart = line.indexOf('{');
+        int braceEnd = line.indexOf('}');
+        if (braceStart > 0 && braceEnd > braceStart) {
+            String name = line.substring(0, braceStart);
+            String tags = line.substring(braceStart, braceEnd + 1);
+            String rest = line.substring(braceEnd + 1).trim();
+            return Line.from(
+                    Span.styled(name, PROM_NAME),
+                    Span.styled(tags, PROM_TAGS),
+                    Span.styled(" " + rest, PROM_VALUE));
+        }
+        // name value (no tags)
+        int space = line.indexOf(' ');
+        if (space > 0) {
+            String name = line.substring(0, space);
+            String value = line.substring(space);
+            return Line.from(
+                    Span.styled(name, PROM_NAME),
+                    Span.styled(value, PROM_VALUE));
+        }
+        return Line.from(Span.raw(line));
+    }
+
+    // ---- Endpoint helpers ----
+
+    private static String findMetricsUrl(IntegrationInfo info) {
+        for (HttpEndpointInfo ep : info.httpEndpoints) {
+            if (ep.management && ep.url != null && 
ep.url.contains("/metrics")) {
+                return ep.url;
+            }
+        }
+        return null;
+    }
+
+    // ---- Meter lookup helpers ----
+
+    private static MicrometerMeterInfo findMeter(List<MicrometerMeterInfo> 
meters, String name) {
+        for (MicrometerMeterInfo m : meters) {
+            if (name.equals(m.name)) {
+                return m;
+            }
+        }
+        return null;
+    }
+
+    private static MicrometerMeterInfo findMeter(
+            List<MicrometerMeterInfo> meters, String name, String tagKey,
+            String tagVal) {
+        for (MicrometerMeterInfo m : meters) {
+            if (name.equals(m.name) && hasTag(m, tagKey, tagVal)) {
+                return m;
+            }
+        }
+        return null;
+    }
+
+    private static List<MicrometerMeterInfo> 
findMeters(List<MicrometerMeterInfo> meters, String namePrefix) {
+        return meters.stream()
+                .filter(m -> m.name != null && m.name.startsWith(namePrefix))
+                .collect(Collectors.toList());
+    }
+
+    private static boolean hasTag(MicrometerMeterInfo m, String key, String 
value) {
+        for (String[] tag : m.tags) {
+            if (key.equals(tag[0]) && value.equals(tag[1])) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private static String tagValue(MicrometerMeterInfo m, String key) {
+        for (String[] tag : m.tags) {
+            if (key.equals(tag[0])) {
+                return tag[1];
+            }
+        }
+        return null;
+    }
+
+    private static long counterValue(List<MicrometerMeterInfo> meters, String 
name) {
+        MicrometerMeterInfo m = findMeter(meters, name);
+        return m != null && m.count != null ? m.count : 0;
+    }
+
+    private static double gaugeValue(List<MicrometerMeterInfo> meters, String 
name) {
+        MicrometerMeterInfo m = findMeter(meters, name);
+        return m != null && m.value != null ? m.value : -1;
+    }
+
+    private static double gaugeValue(List<MicrometerMeterInfo> meters, String 
name, String tagKey, String tagVal) {
+        MicrometerMeterInfo m = findMeter(meters, name, tagKey, tagVal);
+        return m != null && m.value != null ? m.value : 0;
+    }
+
+    private static long gaugeValueLong(List<MicrometerMeterInfo> meters, 
String name) {
+        MicrometerMeterInfo m = findMeter(meters, name);
+        return m != null && m.value != null ? m.value.longValue() : 0;
+    }
+
+    // ---- Formatting helpers ----
+
+    private static String formatNumber(long n) {
+        if (n >= 1_000_000) {
+            return String.format("%.1fM", n / 1_000_000.0);
+        }
+        if (n >= 10_000) {
+            return String.format("%.1fK", n / 1_000.0);
+        }
+        return String.valueOf(n);
+    }
+
+    private static String formatBytes(double bytes) {
+        if (bytes <= 0) {
+            return "0";
+        }
+        if (bytes >= 1_073_741_824) {
+            return String.format("%.1fGB", bytes / 1_073_741_824.0);
+        }
+        if (bytes >= 1_048_576) {
+            return String.format("%.0fMB", bytes / 1_048_576.0);
+        }
+        if (bytes >= 1024) {
+            return String.format("%.0fKB", bytes / 1024.0);
+        }
+        return String.format("%.0fB", bytes);
+    }
+
+    private static String memoryBar(double used, double max, int width) {
+        if (max <= 0) {
+            return "";
+        }
+        double ratio = Math.min(1.0, used / max);
+        int filled = (int) Math.round(ratio * width);
+        return "[" + "▓".repeat(filled) + "░".repeat(width - filled) + "]";
+    }
+
+    // ---- Table mode helpers ----
+
+    private String sortLabel(String label, String column) {
+        return MonitorContext.sortLabel(label, column, sort, sortReversed);
+    }
+
+    private Style sortStyle(String column) {
+        return MonitorContext.sortStyle(column, sort);
+    }
+
+    private int sortMeter(MicrometerMeterInfo a, MicrometerMeterInfo b) {
+        int result = switch (sort) {
+            case "type" -> {
+                String ta = a.type != null ? a.type : "";
+                String tb = b.type != null ? b.type : "";
+                yield ta.compareToIgnoreCase(tb);
+            }
+            case "value" -> Double.compare(numericValue(b), numericValue(a));
+            default -> { // "name"
+                String na = a.name != null ? a.name : "";
+                String nb = b.name != null ? b.name : "";
+                yield na.compareToIgnoreCase(nb);
+            }
+        };
+        return sortReversed ? -result : result;
+    }
+
+    private static double numericValue(MicrometerMeterInfo m) {
+        if (m.count != null) {
+            return m.count;
+        }
+        if (m.value != null) {
+            return m.value;
+        }
+        return 0;
+    }
+
+    private static String typeLabel(String type) {
+        if (type == null) {
+            return "";
+        }
+        return switch (type) {
+            case "counter" -> "counter";
+            case "gauge" -> "gauge";
+            case "timer" -> "timer";
+            case "longTaskTimer" -> "long-task";
+            case "distribution" -> "dist";
+            default -> type;
+        };
+    }
+
+    private static Style typeStyle(String type) {
+        if (type == null) {
+            return Style.EMPTY;
+        }
+        return switch (type) {
+            case "counter" -> Style.EMPTY.fg(Color.LIGHT_BLUE);
+            case "gauge" -> Style.EMPTY.fg(Color.LIGHT_GREEN);
+            case "timer" -> Style.EMPTY.fg(Color.LIGHT_YELLOW);
+            case "longTaskTimer" -> Style.EMPTY.fg(Color.LIGHT_MAGENTA);
+            case "distribution" -> Style.EMPTY.fg(Color.LIGHT_CYAN);
+            default -> Style.EMPTY;
+        };
+    }
+
+    private static String formatValue(MicrometerMeterInfo m) {
+        if (m.type == null) {
+            return "";
+        }
+        return switch (m.type) {
+            case "counter" -> m.count != null ? String.valueOf(m.count) : "0";
+            case "gauge" -> m.value != null ? String.format("%.1f", m.value) : 
"0.0";
+            case "timer" -> String.format("count=%d, mean=%dms, max=%dms",
+                    m.count != null ? m.count : 0,
+                    m.mean != null ? m.mean : 0,
+                    m.max != null ? m.max : 0);
+            case "longTaskTimer" -> String.format("tasks=%d, mean=%dms, 
max=%dms",
+                    m.activeTasks != null ? m.activeTasks : 0,
+                    m.mean != null ? m.mean : 0,
+                    m.max != null ? m.max : 0);
+            case "distribution" -> String.format("count=%d, mean=%.1f, 
max=%.1f",
+                    m.count != null ? m.count : 0,
+                    m.meanDouble != null ? m.meanDouble : 0.0,
+                    m.maxDouble != null ? m.maxDouble : 0.0);
+            default -> "";
+        };
+    }
+
+    private static String formatTags(MicrometerMeterInfo m) {
+        if (m.tags.isEmpty()) {
+            return "";
+        }
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < m.tags.size(); i++) {
+            if (i > 0) {
+                sb.append(", ");
+            }
+            String[] tag = m.tags.get(i);
+            sb.append(tag[0]).append("=").append(tag[1]);
+        }
+        return sb.toString();
+    }
+
+    @Override
+    public SelectionContext getSelectionContext() {
+        IntegrationInfo info = ctx.findSelectedIntegration();
+        if (info == null || info.meters.isEmpty()) {
+            return null;
+        }
+        List<MicrometerMeterInfo> filtered = info.meters;
+        if (!"all".equals(filterType)) {
+            filtered = filtered.stream()
+                    .filter(m -> filterType.equals(m.type))
+                    .collect(Collectors.toList());
+        }
+        List<MicrometerMeterInfo> sorted = new ArrayList<>(filtered);
+        sorted.sort(this::sortMeter);
+        List<String> items = sorted.stream().map(m -> m.name != null ? m.name 
: "").toList();
+        Integer sel = tableState.selected();
+        return new SelectionContext("table", items, sel != null ? sel : -1, 
items.size(), "Metrics");
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MicrometerMeterInfo.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MicrometerMeterInfo.java
new file mode 100644
index 000000000000..2d8fe7f85601
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MicrometerMeterInfo.java
@@ -0,0 +1,41 @@
+/*
+ * 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.List;
+
+class MicrometerMeterInfo {
+    String type;
+    String name;
+    String description;
+    final List<String[]> tags = new ArrayList<>();
+    // counter
+    Long count;
+    // gauge
+    Double value;
+    // timer, longTaskTimer
+    Long mean;
+    Long max;
+    Long total;
+    // longTaskTimer
+    Integer activeTasks;
+    // distribution
+    Double meanDouble;
+    Double maxDouble;
+    Double totalDouble;
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StartupTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StartupTab.java
new file mode 100644
index 000000000000..6854935115d7
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StartupTab.java
@@ -0,0 +1,363 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import dev.tamboui.layout.Constraint;
+import dev.tamboui.layout.Layout;
+import dev.tamboui.layout.Rect;
+import dev.tamboui.style.Color;
+import dev.tamboui.style.Style;
+import dev.tamboui.terminal.Frame;
+import dev.tamboui.text.Line;
+import dev.tamboui.text.Span;
+import dev.tamboui.text.Text;
+import dev.tamboui.tui.event.KeyCode;
+import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.widgets.block.Block;
+import dev.tamboui.widgets.block.BorderType;
+import dev.tamboui.widgets.paragraph.Paragraph;
+import dev.tamboui.widgets.scrollbar.Scrollbar;
+import dev.tamboui.widgets.scrollbar.ScrollbarState;
+import org.apache.camel.dsl.jbang.core.common.PathUtils;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+
+import static org.apache.camel.dsl.jbang.core.commands.tui.MonitorContext.*;
+
+class StartupTab implements MonitorTab {
+
+    private static final Style LABEL = Style.EMPTY.dim();
+    private static final Style VALUE = Style.EMPTY.fg(Color.WHITE).bold();
+    private static final Style HEADER = Style.EMPTY.fg(Color.YELLOW).bold();
+
+    private static final Style[] BAND_STYLES = {
+            Style.EMPTY.fg(Color.GREEN),
+            Style.EMPTY.fg(Color.LIGHT_GREEN),
+            Style.EMPTY.fg(Color.YELLOW),
+            Style.EMPTY.fg(Color.rgb(0xFF, 0xA5, 0x00)),
+            Style.EMPTY.fg(Color.RED),
+    };
+
+    private final MonitorContext ctx;
+    private final ScrollbarState scrollbarState = new ScrollbarState();
+    private final AtomicBoolean loading = new AtomicBoolean(false);
+
+    private List<StartupStep> steps = Collections.emptyList();
+    private int scrollOffset;
+    private long totalDuration;
+    private long maxDuration;
+    private long minDurationColor;
+    private long maxDurationColor;
+    private String errorMessage;
+    private boolean dataLoaded;
+
+    StartupTab(MonitorContext ctx) {
+        this.ctx = ctx;
+    }
+
+    @Override
+    public void onTabSelected() {
+        if (!dataLoaded) {
+            loadStartupData();
+        }
+    }
+
+    @Override
+    public boolean handleKeyEvent(KeyEvent ke) {
+        if (ke.isUp()) {
+            scrollOffset = Math.max(0, scrollOffset - 1);
+            return true;
+        }
+        if (ke.isDown()) {
+            scrollOffset++;
+            return true;
+        }
+        if (ke.isPageUp() || ke.isKey(KeyCode.PAGE_UP)) {
+            scrollOffset = Math.max(0, scrollOffset - 20);
+            return true;
+        }
+        if (ke.isPageDown() || ke.isKey(KeyCode.PAGE_DOWN)) {
+            scrollOffset += 20;
+            return true;
+        }
+        if (ke.isHome()) {
+            scrollOffset = 0;
+            return true;
+        }
+        if (ke.isEnd()) {
+            scrollOffset = Integer.MAX_VALUE;
+            return true;
+        }
+        if (ke.isKey(KeyCode.F5)) {
+            loadStartupData();
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public boolean handleEscape() {
+        return false;
+    }
+
+    @Override
+    public void navigateUp() {
+        scrollOffset = Math.max(0, scrollOffset - 1);
+    }
+
+    @Override
+    public void navigateDown() {
+        scrollOffset++;
+    }
+
+    @Override
+    public void onIntegrationChanged() {
+        steps = Collections.emptyList();
+        scrollOffset = 0;
+        errorMessage = null;
+        dataLoaded = false;
+    }
+
+    @Override
+    public void render(Frame frame, Rect area) {
+        IntegrationInfo info = ctx.findSelectedIntegration();
+        if (info == null) {
+            renderNoSelection(frame, area);
+            return;
+        }
+
+        if (loading.get() && steps.isEmpty()) {
+            frame.renderWidget(
+                    Paragraph.builder()
+                            .text(Text.from(Line.from(Span.styled("  Loading 
startup data...", LABEL))))
+                            
.block(Block.builder().borderType(BorderType.ROUNDED)
+                                    .title(" Startup Timeline ").build())
+                            .build(),
+                    area);
+            return;
+        }
+
+        if (errorMessage != null && steps.isEmpty()) {
+            frame.renderWidget(
+                    Paragraph.builder()
+                            .text(Text.from(Line.from(
+                                    Span.styled("  " + errorMessage, 
Style.EMPTY.fg(Color.LIGHT_RED)))))
+                            
.block(Block.builder().borderType(BorderType.ROUNDED)
+                                    .title(" Startup Timeline ").build())
+                            .build(),
+                    area);
+            return;
+        }
+
+        if (steps.isEmpty()) {
+            frame.renderWidget(
+                    Paragraph.builder()
+                            .text(Text.from(Line.from(
+                                    Span.styled(
+                                            "  No startup data available. The 
integration may not have a startup recorder enabled.",
+                                            LABEL))))
+                            
.block(Block.builder().borderType(BorderType.ROUNDED)
+                                    .title(" Startup Timeline ").build())
+                            .build(),
+                    area);
+            return;
+        }
+
+        String title = String.format(" Startup Timeline — Total: %dms, Steps: 
%d ", totalDuration, steps.size());
+        Block block = Block.builder()
+                .borderType(BorderType.ROUNDED)
+                .title(title)
+                .build();
+        Rect inner = block.inner(area);
+        frame.renderWidget(block, area);
+
+        if (inner.height() < 1 || inner.width() < 10) {
+            return;
+        }
+
+        int visibleLines = inner.height();
+        int maxScroll = Math.max(0, steps.size() - visibleLines);
+        scrollOffset = Math.min(scrollOffset, maxScroll);
+
+        int end = Math.min(scrollOffset + visibleLines, steps.size());
+
+        int barMaxWidth = Math.max(10, inner.width() - 30);
+
+        List<Line> lines = new ArrayList<>();
+        for (int i = scrollOffset; i < end; i++) {
+            lines.add(renderStep(steps.get(i), barMaxWidth));
+        }
+
+        List<Rect> hChunks = Layout.horizontal()
+                .constraints(Constraint.fill(), Constraint.length(1))
+                .split(inner);
+
+        frame.renderWidget(Paragraph.builder().text(Text.from(lines)).build(), 
hChunks.get(0));
+
+        if (steps.size() > visibleLines) {
+            scrollbarState
+                    .contentLength(steps.size())
+                    .viewportContentLength(visibleLines)
+                    .position(scrollOffset);
+            frame.renderStatefulWidget(Scrollbar.builder().build(), 
hChunks.get(1), scrollbarState);
+        }
+    }
+
+    private Line renderStep(StartupStep step, int maxBarWidth) {
+        String indent = "  ".repeat(step.level);
+
+        boolean isRoot = step.level == 0;
+        Style bandStyle = isRoot ? LABEL : colorForDuration(step.duration);
+
+        double ratio = maxDuration > 0 ? (double) step.duration / maxDuration 
: 0;
+        int barWidth = Math.max(1, (int) Math.round(ratio * maxBarWidth));
+        String bar = "█".repeat(barWidth);
+
+        String durationStr = step.duration + "ms";
+        String label = step.name != null ? step.name : "";
+        if (step.description != null && !step.description.isEmpty() && 
!step.description.equals(label)) {
+            label += " " + step.description;
+        }
+
+        int pad = Math.max(1, 8 - durationStr.length());
+
+        return Line.from(
+                Span.raw(indent),
+                Span.styled(bar, bandStyle),
+                Span.raw(" ".repeat(pad)),
+                Span.styled(durationStr, isRoot ? LABEL : VALUE),
+                Span.styled("  " + label, LABEL));
+    }
+
+    private Style colorForDuration(long duration) {
+        if (maxDurationColor <= minDurationColor) {
+            return BAND_STYLES[0];
+        }
+        double ratio = (Math.log1p(duration) - Math.log1p(minDurationColor))
+                       / (Math.log1p(maxDurationColor) - 
Math.log1p(minDurationColor));
+        int bandIndex = Math.min((int) (ratio * 5), 4);
+        return BAND_STYLES[bandIndex];
+    }
+
+    @Override
+    public void renderFooter(List<Span> spans) {
+        hint(spans, "Esc", "back");
+        hint(spans, "↑↓", "scroll");
+        hint(spans, "PgUp/Dn", "page");
+        hintLast(spans, "F5", "reload");
+    }
+
+    private void loadStartupData() {
+        if (ctx.selectedPid == null || ctx.runner == null) {
+            return;
+        }
+        if (!loading.compareAndSet(false, true)) {
+            return;
+        }
+
+        String pid = ctx.selectedPid;
+        ctx.runner.scheduler().execute(() -> {
+            try {
+                Path outputFile = ctx.getOutputFile(pid);
+                PathUtils.deleteFile(outputFile);
+
+                JsonObject root = new JsonObject();
+                root.put("action", "startup-recorder");
+
+                Path actionFile = ctx.getActionFile(pid);
+                PathUtils.writeTextSafely(root.toJson(), actionFile);
+
+                JsonObject jo = pollJsonResponse(outputFile, 5000);
+                PathUtils.deleteFile(outputFile);
+
+                if (jo == null) {
+                    applyResult(Collections.emptyList(), "No response from 
integration");
+                    return;
+                }
+
+                JsonArray stepsArr = (JsonArray) jo.get("steps");
+                if (stepsArr == null || stepsArr.isEmpty()) {
+                    applyResult(Collections.emptyList(), "No startup steps 
available");
+                    return;
+                }
+
+                List<StartupStep> parsed = new ArrayList<>();
+                for (Object o : stepsArr) {
+                    JsonObject sj = (JsonObject) o;
+                    StartupStep s = new StartupStep();
+                    s.id = sj.getIntegerOrDefault("id", 0);
+                    s.parentId = sj.getIntegerOrDefault("parentId", 0);
+                    s.level = sj.getIntegerOrDefault("level", 0);
+                    s.name = sj.getString("name");
+                    s.type = sj.getString("type");
+                    s.description = sj.getString("description");
+                    s.beginTime = TuiHelper.objToLong(sj.get("beginTime"));
+                    s.duration = TuiHelper.objToLong(sj.get("duration"));
+                    parsed.add(s);
+                }
+
+                applyResult(parsed, null);
+            } catch (Exception e) {
+                applyResult(Collections.emptyList(), "Error: " + 
e.getMessage());
+            } finally {
+                loading.set(false);
+            }
+        });
+    }
+
+    private void applyResult(List<StartupStep> parsed, String error) {
+        if (ctx.runner == null) {
+            return;
+        }
+        ctx.runner.runOnRenderThread(() -> {
+            steps = parsed;
+            errorMessage = error;
+            dataLoaded = true;
+
+            if (!steps.isEmpty()) {
+                totalDuration = steps.stream().mapToLong(s -> 
s.duration).max().orElse(0);
+                maxDuration = totalDuration;
+                minDurationColor
+                        = steps.stream().filter(s -> s.level > 0).mapToLong(s 
-> s.duration).min().orElse(0);
+                maxDurationColor
+                        = steps.stream().filter(s -> s.level > 0).mapToLong(s 
-> s.duration).max().orElse(0);
+            }
+        });
+    }
+
+    @Override
+    public SelectionContext getSelectionContext() {
+        return null;
+    }
+
+    static class StartupStep {
+        int id;
+        int parentId;
+        int level;
+        String name;
+        String type;
+        String description;
+        long beginTime;
+        long duration;
+    }
+}

Reply via email to