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 1cbab2188bd7 CAMEL-24328: Show JFR runtime data in TUI JFR tab
1cbab2188bd7 is described below

commit 1cbab2188bd72163951baad8b23493f94553ebca
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Aug 3 22:52:46 2026 +0200

    CAMEL-24328: Show JFR runtime data in TUI JFR tab
    
    Add snapshot command to CamelJfrDevConsole that captures a point-in-time
    JFR recording and aggregates Camel runtime events into per-route,
    per-processor, and per-endpoint duration statistics, plus recent failures
    and redeliveries. Show the data in the TUI JFR tab with five navigable
    views (Routes, Processors, Endpoints, Failures, Redeliveries), split
    layout with processor drill-down, sorting, auto-snapshot on tab open,
    and performance bars with timing-aware styling. Expose data via
    getTableDataAsJson() for MCP/AI agents. Move JFR tab to Observability
    group. Fix CamelContextAware injection for 
FlightRecorderStartupStepRecorder.
    
    Closes #25312
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../org/apache/camel/catalog/dev-consoles/jfr.json |   32 +-
 .../org/apache/camel/catalog/docs/jfr.adoc         |   57 +-
 .../META-INF/org/apache/camel/dev-console/jfr.json |   32 +-
 components/camel-jfr/src/main/docs/jfr.adoc        |   57 +-
 .../camel/runtime/jfr/CamelJfrDevConsole.java      |  251 ++++-
 .../jfr/FlightRecorderStartupStepRecorder.java     |    4 +
 .../camel/runtime/jfr/CamelJfrDevConsoleTest.java  |   76 ++
 .../camel/impl/engine/AbstractCamelContext.java    |    1 +
 .../modules/ROOT/pages/camel-jbang-tui.adoc        |   32 +-
 .../camel/cli/connector/LocalCliConnector.java     |    8 +
 .../camel/dsl/jbang/core/commands/tui/JfrTab.java  | 1112 +++++++++++++++++---
 .../dsl/jbang/core/commands/tui/TabRegistry.java   |    6 +-
 .../jbang/core/commands/tui/JfrTabRenderTest.java  |  187 +++-
 13 files changed, 1696 insertions(+), 159 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/jfr.json
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/jfr.json
index 0886083d6657..1d7b21b30794 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/jfr.json
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/jfr.json
@@ -25,7 +25,8 @@
         "status",
         "enable",
         "disable",
-        "jfc"
+        "jfc",
+        "snapshot"
       ],
       "deprecated": false,
       "autowired": false,
@@ -70,6 +71,35 @@
       "secret": false,
       "defaultValue": "all",
       "description": "The runtime event to enable or disable, or all for every 
event"
+    },
+    "limit": {
+      "index": 3,
+      "kind": "option",
+      "displayName": "Limit",
+      "group": "query",
+      "label": "query",
+      "required": false,
+      "type": "integer",
+      "javaType": "int",
+      "deprecated": false,
+      "autowired": false,
+      "secret": false,
+      "defaultValue": 50,
+      "description": "Maximum number of failure and redelivery entries to 
return in a snapshot"
+    },
+    "routeId": {
+      "index": 4,
+      "kind": "option",
+      "displayName": "Route Id",
+      "group": "query",
+      "label": "query",
+      "required": false,
+      "type": "string",
+      "javaType": "java.lang.String",
+      "deprecated": false,
+      "autowired": false,
+      "secret": false,
+      "description": "Filter snapshot results to the given route id"
     }
   }
 }
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/jfr.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/jfr.adoc
index 91f09529eecc..fff95280def1 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/jfr.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/jfr.adoc
@@ -119,7 +119,7 @@ JVM-wide recorder settings such as the repository path and 
stack depth.
 
 === The `jfr` dev console
 
-When `camel-jfr` is on the classpath, a `jfr` dev console is available with 
four commands
+When `camel-jfr` is on the classpath, a `jfr` dev console is available with 
five commands
 (pass as the `command` option):
 
 [cols="1,3",options="header"]
@@ -138,6 +138,11 @@ When `camel-jfr` is on the classpath, a `jfr` dev console 
is available with four
 | `jfc`
 | Generates a `.jfc` overlay for the six events, honoring an optional 
comma-separated
   `disable` option, and a ready-to-copy `jcmd JFR.start settings=...` line.
+
+| `snapshot`
+| Takes a point-in-time snapshot of the active JFR recording and aggregates 
Camel
+  runtime events into per-route, per-processor, and per-endpoint duration 
statistics,
+  plus recent failures and redeliveries. Accepts optional `routeId` and 
`limit` options.
 |===
 
 The console is reachable wherever dev consoles are, for example over HTTP when 
the
@@ -150,3 +155,53 @@ curl "http://localhost:8080/q/dev/jfr?command=status";
 
 It is also available as a *JFR* tab in the CLI terminal UI (`camel tui`), 
which renders
 the status and binds `E` / `D` / `J` to enable all, disable all, and generate 
a `.jfc`.
+
+=== Snapshot
+
+The `snapshot` command captures the current state of a running JFR recording 
without
+stopping it, and returns aggregated statistics for Camel runtime events. This 
gives you
+a lightweight alternative to exporting a `.jfr` file and opening it in Java 
Mission Control.
+
+A recording must be active for the snapshot to contain data. If no recording 
is running,
+the command returns an error message.
+
+==== Options
+
+[cols="1,1,3",options="header"]
+|===
+| Option | Default | Description
+| `routeId` | _(all)_ | Filter results to a single route. Applies to routes, 
processors, failures, and redeliveries.
+| `limit` | `50` | Maximum number of failure and redelivery entries to return.
+|===
+
+==== Example
+
+[source,bash]
+----
+curl "http://localhost:8080/q/dev/jfr?command=snapshot";
+curl 
"http://localhost:8080/q/dev/jfr?command=snapshot&routeId=order-in&limit=20";
+----
+
+==== Response
+
+The JSON response contains five sections:
+
+[cols="1,3",options="header"]
+|===
+| Section | Description
+| `routes` | Per-route totals, failure counts, and min/mean/max duration in 
milliseconds. Sorted by total descending.
+| `processors` | Per-processor statistics with processor type and owning 
route. Sorted by mean duration descending (slowest first).
+| `endpoints` | Per-endpoint send statistics with duration. Sorted by total 
descending.
+| `failures` | Recent exchange failures with exception type and message. 
Newest first, capped at `limit`.
+| `redeliveries` | Recent redelivery attempts with attempt number and max 
attempts. Newest first, capped at `limit`.
+|===
+
+The top-level `eventCount` field reports the total number of Camel events 
found in the
+snapshot.
+
+==== TUI integration
+
+In the CLI terminal UI (`camel tui`), the *JFR* tab shows the snapshot data in 
five
+navigable table views. Press `F5` to take a snapshot, and use keys `1`–`5` to 
switch
+between Routes, Processors, Endpoints, Failures, and Redeliveries. Press 
`Enter` on
+a route to drill down into its processors, and `Esc` to go back.
diff --git 
a/components/camel-jfr/src/generated/resources/META-INF/org/apache/camel/dev-console/jfr.json
 
b/components/camel-jfr/src/generated/resources/META-INF/org/apache/camel/dev-console/jfr.json
index 0886083d6657..1d7b21b30794 100644
--- 
a/components/camel-jfr/src/generated/resources/META-INF/org/apache/camel/dev-console/jfr.json
+++ 
b/components/camel-jfr/src/generated/resources/META-INF/org/apache/camel/dev-console/jfr.json
@@ -25,7 +25,8 @@
         "status",
         "enable",
         "disable",
-        "jfc"
+        "jfc",
+        "snapshot"
       ],
       "deprecated": false,
       "autowired": false,
@@ -70,6 +71,35 @@
       "secret": false,
       "defaultValue": "all",
       "description": "The runtime event to enable or disable, or all for every 
event"
+    },
+    "limit": {
+      "index": 3,
+      "kind": "option",
+      "displayName": "Limit",
+      "group": "query",
+      "label": "query",
+      "required": false,
+      "type": "integer",
+      "javaType": "int",
+      "deprecated": false,
+      "autowired": false,
+      "secret": false,
+      "defaultValue": 50,
+      "description": "Maximum number of failure and redelivery entries to 
return in a snapshot"
+    },
+    "routeId": {
+      "index": 4,
+      "kind": "option",
+      "displayName": "Route Id",
+      "group": "query",
+      "label": "query",
+      "required": false,
+      "type": "string",
+      "javaType": "java.lang.String",
+      "deprecated": false,
+      "autowired": false,
+      "secret": false,
+      "description": "Filter snapshot results to the given route id"
     }
   }
 }
diff --git a/components/camel-jfr/src/main/docs/jfr.adoc 
b/components/camel-jfr/src/main/docs/jfr.adoc
index 91f09529eecc..fff95280def1 100644
--- a/components/camel-jfr/src/main/docs/jfr.adoc
+++ b/components/camel-jfr/src/main/docs/jfr.adoc
@@ -119,7 +119,7 @@ JVM-wide recorder settings such as the repository path and 
stack depth.
 
 === The `jfr` dev console
 
-When `camel-jfr` is on the classpath, a `jfr` dev console is available with 
four commands
+When `camel-jfr` is on the classpath, a `jfr` dev console is available with 
five commands
 (pass as the `command` option):
 
 [cols="1,3",options="header"]
@@ -138,6 +138,11 @@ When `camel-jfr` is on the classpath, a `jfr` dev console 
is available with four
 | `jfc`
 | Generates a `.jfc` overlay for the six events, honoring an optional 
comma-separated
   `disable` option, and a ready-to-copy `jcmd JFR.start settings=...` line.
+
+| `snapshot`
+| Takes a point-in-time snapshot of the active JFR recording and aggregates 
Camel
+  runtime events into per-route, per-processor, and per-endpoint duration 
statistics,
+  plus recent failures and redeliveries. Accepts optional `routeId` and 
`limit` options.
 |===
 
 The console is reachable wherever dev consoles are, for example over HTTP when 
the
@@ -150,3 +155,53 @@ curl "http://localhost:8080/q/dev/jfr?command=status";
 
 It is also available as a *JFR* tab in the CLI terminal UI (`camel tui`), 
which renders
 the status and binds `E` / `D` / `J` to enable all, disable all, and generate 
a `.jfc`.
+
+=== Snapshot
+
+The `snapshot` command captures the current state of a running JFR recording 
without
+stopping it, and returns aggregated statistics for Camel runtime events. This 
gives you
+a lightweight alternative to exporting a `.jfr` file and opening it in Java 
Mission Control.
+
+A recording must be active for the snapshot to contain data. If no recording 
is running,
+the command returns an error message.
+
+==== Options
+
+[cols="1,1,3",options="header"]
+|===
+| Option | Default | Description
+| `routeId` | _(all)_ | Filter results to a single route. Applies to routes, 
processors, failures, and redeliveries.
+| `limit` | `50` | Maximum number of failure and redelivery entries to return.
+|===
+
+==== Example
+
+[source,bash]
+----
+curl "http://localhost:8080/q/dev/jfr?command=snapshot";
+curl 
"http://localhost:8080/q/dev/jfr?command=snapshot&routeId=order-in&limit=20";
+----
+
+==== Response
+
+The JSON response contains five sections:
+
+[cols="1,3",options="header"]
+|===
+| Section | Description
+| `routes` | Per-route totals, failure counts, and min/mean/max duration in 
milliseconds. Sorted by total descending.
+| `processors` | Per-processor statistics with processor type and owning 
route. Sorted by mean duration descending (slowest first).
+| `endpoints` | Per-endpoint send statistics with duration. Sorted by total 
descending.
+| `failures` | Recent exchange failures with exception type and message. 
Newest first, capped at `limit`.
+| `redeliveries` | Recent redelivery attempts with attempt number and max 
attempts. Newest first, capped at `limit`.
+|===
+
+The top-level `eventCount` field reports the total number of Camel events 
found in the
+snapshot.
+
+==== TUI integration
+
+In the CLI terminal UI (`camel tui`), the *JFR* tab shows the snapshot data in 
five
+navigable table views. Press `F5` to take a snapshot, and use keys `1`–`5` to 
switch
+between Routes, Processors, Endpoints, Failures, and Redeliveries. Press 
`Enter` on
+a route to drill down into its processors, and `Esc` to go back.
diff --git 
a/components/camel-jfr/src/main/java/org/apache/camel/runtime/jfr/CamelJfrDevConsole.java
 
b/components/camel-jfr/src/main/java/org/apache/camel/runtime/jfr/CamelJfrDevConsole.java
index 0f19b4b25ec8..3aeff394ee6c 100644
--- 
a/components/camel-jfr/src/main/java/org/apache/camel/runtime/jfr/CamelJfrDevConsole.java
+++ 
b/components/camel-jfr/src/main/java/org/apache/camel/runtime/jfr/CamelJfrDevConsole.java
@@ -16,8 +16,14 @@
  */
 package org.apache.camel.runtime.jfr;
 
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.time.Duration;
 import java.util.ArrayList;
+import java.util.Comparator;
 import java.util.EnumSet;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -25,11 +31,14 @@ import java.util.Set;
 import jdk.jfr.FlightRecorder;
 import jdk.jfr.Recording;
 import jdk.jfr.RecordingState;
+import jdk.jfr.consumer.RecordedEvent;
+import jdk.jfr.consumer.RecordingFile;
 import org.apache.camel.CamelContext;
 import org.apache.camel.spi.LifecycleStrategy;
 import org.apache.camel.spi.Metadata;
 import org.apache.camel.spi.annotations.DevConsole;
 import org.apache.camel.support.console.AbstractDevConsole;
+import org.apache.camel.util.StringHelper;
 import org.apache.camel.util.json.JsonArray;
 import org.apache.camel.util.json.JsonObject;
 
@@ -44,7 +53,7 @@ import org.apache.camel.util.json.JsonObject;
 public class CamelJfrDevConsole extends AbstractDevConsole {
 
     @Metadata(label = "query", description = "Command to perform", javaType = 
"java.lang.String",
-              defaultValue = "status", enums = "status,enable,disable,jfc")
+              defaultValue = "status", enums = 
"status,enable,disable,jfc,snapshot")
     public static final String COMMAND = "command";
 
     @Metadata(label = "query", description = "The runtime event to enable or 
disable, or all for every event",
@@ -57,7 +66,17 @@ public class CamelJfrDevConsole extends AbstractDevConsole {
               javaType = "java.lang.String")
     public static final String DISABLE = "disable";
 
+    @Metadata(label = "query", description = "Filter snapshot results to the 
given route id",
+              javaType = "java.lang.String")
+    public static final String ROUTE_ID = "routeId";
+
+    @Metadata(label = "query",
+              description = "Maximum number of failure and redelivery entries 
to return in a snapshot",
+              javaType = "int", defaultValue = "50")
+    public static final String LIMIT = "limit";
+
     private static final String ALL = "all";
+    private static final int DEFAULT_LIMIT = 50;
 
     public CamelJfrDevConsole() {
         super("camel", "jfr", "JFR Runtime Instrumentation",
@@ -94,7 +113,7 @@ public class CamelJfrDevConsole extends AbstractDevConsole {
         List<Recording> recordings = 
FlightRecorder.getFlightRecorder().getRecordings();
 
         StringBuilder sb = new StringBuilder();
-        sb.append("registered: 
").append(isInstrumentationRegistered()).append('\n');
+        sb.append("runtimeEvents: 
").append(isInstrumentationRegistered()).append('\n');
         if (recordings.isEmpty()) {
             sb.append("recordings: none active\n");
         } else {
@@ -199,6 +218,224 @@ public class CamelJfrDevConsole extends 
AbstractDevConsole {
                + "\n(replace 'default' with whatever base profile your 
recording already uses)";
     }
 
+    // ---- snapshot ----
+
+    private static class DurationStats {
+        long count;
+        long failed;
+        long minNanos = Long.MAX_VALUE;
+        long maxNanos;
+        long totalNanos;
+
+        void record(long nanos, boolean isFailed) {
+            count++;
+            if (isFailed) {
+                failed++;
+            }
+            totalNanos += nanos;
+            if (nanos < minNanos) {
+                minNanos = nanos;
+            }
+            if (nanos > maxNanos) {
+                maxNanos = nanos;
+            }
+        }
+
+        double minMs() {
+            return count == 0 ? 0 : minNanos / 1_000_000.0;
+        }
+
+        double meanMs() {
+            return count == 0 ? 0 : (totalNanos / (double) count) / 
1_000_000.0;
+        }
+
+        double maxMs() {
+            return maxNanos / 1_000_000.0;
+        }
+
+        JsonObject toJson() {
+            JsonObject jo = new JsonObject();
+            jo.put("total", count);
+            jo.put("failed", failed);
+            jo.put("minMs", round3(minMs()));
+            jo.put("meanMs", round3(meanMs()));
+            jo.put("maxMs", round3(maxMs()));
+            return jo;
+        }
+    }
+
+    private static double round3(double value) {
+        return Math.round(value * 1000.0) / 1000.0;
+    }
+
+    private JsonObject doSnapshot(Map<String, Object> options) {
+        String routeIdFilter = optionString(options, ROUTE_ID);
+        int limit = DEFAULT_LIMIT;
+        String limitStr = optionString(options, LIMIT);
+        if (limitStr != null) {
+            try {
+                limit = Integer.parseInt(limitStr);
+            } catch (NumberFormatException e) {
+                // keep default
+            }
+        }
+
+        JsonObject result = new JsonObject();
+        result.put("snapshot", true);
+        result.put("snapshotTimestamp", System.currentTimeMillis());
+
+        Path tempFile = null;
+        try (Recording snapshot = 
FlightRecorder.getFlightRecorder().takeSnapshot()) {
+            if (snapshot.getSize() == 0) {
+                result.put("error", "no JFR data available: ensure a recording 
is active");
+                return result;
+            }
+            tempFile = Files.createTempFile("camel-jfr-snapshot-", ".jfr");
+            snapshot.dump(tempFile);
+
+            Map<String, DurationStats> routeStats = new LinkedHashMap<>();
+            Map<String, DurationStats> processorStats = new LinkedHashMap<>();
+            Map<String, String> processorTypes = new LinkedHashMap<>();
+            Map<String, String> processorRoutes = new LinkedHashMap<>();
+            Map<String, DurationStats> endpointStats = new LinkedHashMap<>();
+            List<JsonObject> failures = new ArrayList<>();
+            List<JsonObject> redeliveries = new ArrayList<>();
+            int eventCount = 0;
+
+            String routeEventName = CamelJfrEvents.ROUTE.getEventName();
+            String processorEventName = 
CamelJfrEvents.PROCESSOR.getEventName();
+            String sendEventName = CamelJfrEvents.SEND.getEventName();
+            String failedEventName = CamelJfrEvents.FAILED.getEventName();
+            String redeliveryEventName = 
CamelJfrEvents.REDELIVERY.getEventName();
+
+            try (RecordingFile rf = new RecordingFile(tempFile)) {
+                while (rf.hasMoreEvents()) {
+                    RecordedEvent event = rf.readEvent();
+                    String eventName = event.getEventType().getName();
+
+                    if (routeEventName.equals(eventName)) {
+                        String routeId = event.getString("routeId");
+                        if (routeIdFilter == null || 
routeIdFilter.equals(routeId)) {
+                            long nanos = durationNanos(event);
+                            boolean failed = event.getBoolean("failed");
+                            routeStats.computeIfAbsent(routeId, k -> new 
DurationStats()).record(nanos, failed);
+                            eventCount++;
+                        }
+                    } else if (processorEventName.equals(eventName)) {
+                        String routeId = event.getString("routeId");
+                        if (routeIdFilter == null || 
routeIdFilter.equals(routeId)) {
+                            String processorId = 
event.getString("processorId");
+                            long nanos = durationNanos(event);
+                            boolean failed = event.getBoolean("failed");
+                            processorStats.computeIfAbsent(processorId, k -> 
new DurationStats()).record(nanos, failed);
+                            processorTypes.putIfAbsent(processorId, 
event.getString("processorType"));
+                            processorRoutes.putIfAbsent(processorId, routeId);
+                            eventCount++;
+                        }
+                    } else if (sendEventName.equals(eventName)) {
+                        String endpointUri = event.getString("endpointUri");
+                        long nanos = durationNanos(event);
+                        boolean failed = event.getBoolean("failed");
+                        endpointStats.computeIfAbsent(endpointUri, k -> new 
DurationStats()).record(nanos, failed);
+                        eventCount++;
+                    } else if (failedEventName.equals(eventName)) {
+                        String routeId = event.getString("routeId");
+                        if (routeIdFilter == null || 
routeIdFilter.equals(routeId)) {
+                            JsonObject fo = new JsonObject();
+                            fo.put("timestamp", 
event.getStartTime().toString());
+                            fo.put("exchangeId", 
event.getString("exchangeId"));
+                            fo.put("routeId", routeId);
+                            fo.put("exceptionType", 
event.getString("exceptionType"));
+                            fo.put("exceptionMessage", 
event.getString("exceptionMessage"));
+                            failures.add(fo);
+                            eventCount++;
+                        }
+                    } else if (redeliveryEventName.equals(eventName)) {
+                        String routeId = event.getString("routeId");
+                        if (routeIdFilter == null || 
routeIdFilter.equals(routeId)) {
+                            JsonObject ro = new JsonObject();
+                            ro.put("timestamp", 
event.getStartTime().toString());
+                            ro.put("exchangeId", 
event.getString("exchangeId"));
+                            ro.put("routeId", routeId);
+                            ro.put("attempt", event.getInt("attempt"));
+                            ro.put("maxAttempts", event.getInt("maxAttempts"));
+                            redeliveries.add(ro);
+                            eventCount++;
+                        }
+                    }
+                }
+            }
+
+            result.put("eventCount", eventCount);
+
+            // routes — sorted by total descending
+            JsonArray routesJson = new JsonArray();
+            routeStats.entrySet().stream()
+                    .sorted(Comparator.<Map.Entry<String, DurationStats>> 
comparingLong(e -> e.getValue().count).reversed())
+                    .forEach(e -> {
+                        JsonObject jo = e.getValue().toJson();
+                        jo.put("routeId", e.getKey());
+                        routesJson.add(jo);
+                    });
+            result.put("routes", routesJson);
+
+            // processors — sorted by mean duration descending (slowest first)
+            JsonArray processorsJson = new JsonArray();
+            processorStats.entrySet().stream()
+                    .sorted(Comparator.<Map.Entry<String, DurationStats>> 
comparingDouble(
+                            e -> e.getValue().meanMs()).reversed())
+                    .forEach(e -> {
+                        JsonObject jo = e.getValue().toJson();
+                        jo.put("processorId", e.getKey());
+                        jo.put("processorType", 
processorTypes.get(e.getKey()));
+                        jo.put("routeId", processorRoutes.get(e.getKey()));
+                        processorsJson.add(jo);
+                    });
+            result.put("processors", processorsJson);
+
+            // endpoints — sorted by total descending
+            JsonArray endpointsJson = new JsonArray();
+            endpointStats.entrySet().stream()
+                    .sorted(Comparator.<Map.Entry<String, DurationStats>> 
comparingLong(
+                            e -> e.getValue().count).reversed())
+                    .forEach(e -> {
+                        JsonObject jo = e.getValue().toJson();
+                        jo.put("endpointUri", e.getKey());
+                        endpointsJson.add(jo);
+                    });
+            result.put("endpoints", endpointsJson);
+
+            // failures — newest first, capped at limit
+            failures.sort(Comparator.comparing((JsonObject o) -> 
o.getString("timestamp")).reversed());
+            JsonArray failuresJson = new JsonArray();
+            failures.stream().limit(limit).forEach(failuresJson::add);
+            result.put("failures", failuresJson);
+
+            // redeliveries — newest first, capped at limit
+            redeliveries.sort(Comparator.comparing((JsonObject o) -> 
o.getString("timestamp")).reversed());
+            JsonArray redeliveriesJson = new JsonArray();
+            redeliveries.stream().limit(limit).forEach(redeliveriesJson::add);
+            result.put("redeliveries", redeliveriesJson);
+
+        } catch (IOException e) {
+            result.put("error", "failed to read JFR snapshot: " + 
e.getMessage());
+        } finally {
+            if (tempFile != null) {
+                try {
+                    Files.deleteIfExists(tempFile);
+                } catch (IOException ignored) {
+                }
+            }
+        }
+
+        return result;
+    }
+
+    private static long durationNanos(RecordedEvent event) {
+        Duration d = event.getDuration();
+        return d != null ? d.toNanos() : 0;
+    }
+
     @Override
     protected String doCallText(Map<String, Object> options) {
         String command = optionString(options, COMMAND);
@@ -213,6 +450,7 @@ public class CamelJfrDevConsole extends AbstractDevConsole {
                     yield e.getMessage();
                 }
             }
+            case "snapshot" -> doSnapshot(options).toJson();
             default -> unknownCommand(command);
         };
     }
@@ -223,12 +461,12 @@ public class CamelJfrDevConsole extends 
AbstractDevConsole {
         JsonObject root = new JsonObject();
         switch (command != null ? command : "status") {
             case "status" -> {
-                root.put("registered", isInstrumentationRegistered());
+                root.put("runtimeEvents", isInstrumentationRegistered());
                 JsonArray recordingsJson = new JsonArray();
                 for (Recording recording : 
FlightRecorder.getFlightRecorder().getRecordings()) {
                     JsonObject rec = new JsonObject();
                     rec.put("name", recording.getName());
-                    rec.put("state", recording.getState().toString());
+                    rec.put("state", 
StringHelper.capitalize(recording.getState().toString().toLowerCase(java.util.Locale.US)));
                     rec.put("destination",
                             recording.getDestination() != null ? 
recording.getDestination().toString() : null);
                     recordingsJson.add(rec);
@@ -252,12 +490,15 @@ public class CamelJfrDevConsole extends 
AbstractDevConsole {
                     root.put("error", e.getMessage());
                 }
             }
+            case "snapshot" -> {
+                return doSnapshot(options);
+            }
             default -> root.put("error", unknownCommand(command));
         }
         return root;
     }
 
     private static String unknownCommand(String command) {
-        return "unknown command: " + command + ". Valid values: status, 
enable, disable, jfc";
+        return "unknown command: " + command + ". Valid values: status, 
enable, disable, jfc, snapshot";
     }
 }
diff --git 
a/components/camel-jfr/src/main/java/org/apache/camel/startup/jfr/FlightRecorderStartupStepRecorder.java
 
b/components/camel-jfr/src/main/java/org/apache/camel/startup/jfr/FlightRecorderStartupStepRecorder.java
index 33c2ba76a731..494a5a2a5630 100644
--- 
a/components/camel-jfr/src/main/java/org/apache/camel/startup/jfr/FlightRecorderStartupStepRecorder.java
+++ 
b/components/camel-jfr/src/main/java/org/apache/camel/startup/jfr/FlightRecorderStartupStepRecorder.java
@@ -73,6 +73,10 @@ public class FlightRecorderStartupStepRecorder extends 
DefaultStartupStepRecorde
     @Override
     public void setRuntimeEnabled(boolean runtimeEnabled) {
         this.runtimeEnabled = runtimeEnabled;
+        if (runtimeEnabled && camelContext != null && instrumentation == null) 
{
+            instrumentation = new CamelJfrRuntimeInstrumentation();
+            camelContext.addLifecycleStrategy(instrumentation);
+        }
     }
 
     @Override
diff --git 
a/components/camel-jfr/src/test/java/org/apache/camel/runtime/jfr/CamelJfrDevConsoleTest.java
 
b/components/camel-jfr/src/test/java/org/apache/camel/runtime/jfr/CamelJfrDevConsoleTest.java
index 0965a4c03890..eb9e105904aa 100644
--- 
a/components/camel-jfr/src/test/java/org/apache/camel/runtime/jfr/CamelJfrDevConsoleTest.java
+++ 
b/components/camel-jfr/src/test/java/org/apache/camel/runtime/jfr/CamelJfrDevConsoleTest.java
@@ -26,6 +26,7 @@ import org.apache.camel.impl.DefaultCamelContext;
 import org.apache.camel.startup.jfr.FlightRecorderStartupStepRecorder;
 import org.apache.camel.support.PluginHelper;
 import org.apache.camel.test.junit6.CamelTestSupport;
+import org.apache.camel.util.json.JsonArray;
 import org.apache.camel.util.json.JsonObject;
 import org.junit.jupiter.api.Test;
 
@@ -256,4 +257,79 @@ class CamelJfrDevConsoleTest extends CamelTestSupport {
         assertThat(json.getString("error")).contains("unknown event: bogus");
         assertThat(json.get("jfc")).isNull();
     }
+
+    @Test
+    void snapshotReturnsAggregatedDataFromActiveRecording() throws Exception {
+        try (DefaultCamelContext ctx = new DefaultCamelContext(false)) {
+            FlightRecorderStartupStepRecorder recorder = new 
FlightRecorderStartupStepRecorder();
+            recorder.setRuntimeEnabled(true);
+            recorder.setCamelContext(ctx);
+            ctx.getCamelContextExtension().setStartupStepRecorder(recorder);
+            ctx.addRoutes(new RouteBuilder() {
+                @Override
+                public void configure() {
+                    
from("direct:snap").routeId("snap-route").to("mock:snap-result");
+                }
+            });
+            ctx.build();
+            ctx.start();
+
+            try (Recording recording = startRecordingWithAllEvents()) {
+                ctx.createProducerTemplate().sendBody("direct:snap", "hello");
+                ctx.createProducerTemplate().sendBody("direct:snap", "world");
+
+                CamelJfrDevConsole console = resolveConsole(ctx);
+                JsonObject json = (JsonObject) 
console.call(DevConsole.MediaType.JSON, Map.of("command", "snapshot"));
+
+                assertThat(json.getBoolean("snapshot")).isTrue();
+                assertThat(json.getInteger("eventCount")).isGreaterThan(0);
+
+                JsonArray routes = json.getJsonArray("routes");
+                assertThat(routes).isNotNull().isNotEmpty();
+
+                JsonArray processors = json.getJsonArray("processors");
+                assertThat(processors).isNotNull();
+
+                JsonArray endpoints = json.getJsonArray("endpoints");
+                assertThat(endpoints).isNotNull();
+            }
+        }
+    }
+
+    @Test
+    void snapshotTextFormatContainsRouteData() throws Exception {
+        try (DefaultCamelContext ctx = new DefaultCamelContext(false)) {
+            FlightRecorderStartupStepRecorder recorder = new 
FlightRecorderStartupStepRecorder();
+            recorder.setRuntimeEnabled(true);
+            recorder.setCamelContext(ctx);
+            ctx.getCamelContextExtension().setStartupStepRecorder(recorder);
+            ctx.addRoutes(new RouteBuilder() {
+                @Override
+                public void configure() {
+                    
from("direct:txt").routeId("txt-route").to("mock:txt-result");
+                }
+            });
+            ctx.build();
+            ctx.start();
+
+            try (Recording recording = startRecordingWithAllEvents()) {
+                ctx.createProducerTemplate().sendBody("direct:txt", "test");
+
+                CamelJfrDevConsole console = resolveConsole(ctx);
+                String text = (String) console.call(DevConsole.MediaType.TEXT, 
Map.of("command", "snapshot"));
+
+                assertThat(text).contains("snapshot").contains("txt-route");
+            }
+        }
+    }
+
+    @Test
+    void snapshotWithNoRecordingReturnsErrorMessage() {
+        CamelJfrDevConsole console = resolveConsole(context);
+
+        JsonObject json = (JsonObject) console.call(DevConsole.MediaType.JSON, 
Map.of("command", "snapshot"));
+
+        assertThat(json.getBoolean("snapshot")).isTrue();
+        assertThat(json.getString("error")).contains("no JFR data available");
+    }
 }
diff --git 
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/AbstractCamelContext.java
 
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/AbstractCamelContext.java
index 0875b7af7d7c..0bc06ccd0635 100644
--- 
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/AbstractCamelContext.java
+++ 
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/AbstractCamelContext.java
@@ -2447,6 +2447,7 @@ public abstract class AbstractCamelContext extends 
BaseService
             }
         }
 
+        CamelContextAware.trySetCamelContext(startupStepRecorder, this);
         startupStepRecorder.start();
         StartupStep step = startupStepRecorder.beginStep(CamelContext.class, 
null, "Build CamelContext");
 
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
index 6cd09de2a64c..5ce703da222e 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
@@ -124,9 +124,9 @@ to any tab, or use *Tab* / *Shift+Tab* to cycle.
 The *More* menu (key *0*) opens a popup with tabs organized into groups:
 
 * *Routing* -- Browse Endpoints, Consumers, HTTP, Inflight, Producers, Route 
Controller
-* *Observability* -- Circuit Breaker, Health, Metrics, Network Services, 
Exchange Events, Recovery Tasks, OpenTelemetry Spans
+* *Observability* -- Circuit Breaker, Health, JFR, Metrics, Network Services, 
Exchange Events, Recovery Tasks, OpenTelemetry Spans
 * *Data* -- JDBC DataSource, Kafka, SQL Query, SQL Trace
-* *JVM* -- Classpath, Heap Memory Histogram, JFR, Memory Usage, Memory Leak, 
Process, Startup, Threads
+* *JVM* -- Classpath, Heap Memory Histogram, Memory Usage, Memory Leak, 
Process, Startup, Threads
 * *Project* -- Beans, Catalog, Configuration, CVE Audit, Maven Dependencies, 
Type Converters, Data Type Transformers
 
 Tabs appear dynamically based on what the integration uses. For example, the 
Kafka tab appears
@@ -608,6 +608,34 @@ in your running integration. It runs two sequential 
recordings and compares obje
 classifying each class as growing, stable, shrinking, new, or gone. This is 
lightweight and safe
 for production use.
 
+=== JFR Runtime Profiling
+
+The JFR tab (under More > Observability) gives you a lightweight profiling 
view of your running integration
+using Java Flight Recorder. JFR is built into the JVM with very low overhead, 
making it suitable
+for near-production use cases where you need timing data without impacting 
throughput.
+
+Start your integration with JFR runtime instrumentation enabled:
+
+[source,bash]
+----
+camel run my-route.yaml --jfr
+----
+
+The JFR tab shows the recording status and event configuration. Press *F5* to 
take a snapshot
+of the active recording -- the TUI aggregates all Camel events and presents 
them in five views:
+
+* *Routes* (key *1*) -- per-route totals, failure counts, and min/mean/max 
duration
+* *Processors* (key *2*) -- per-processor statistics with type and owning 
route, sorted by slowest first
+* *Endpoints* (key *3*) -- per-endpoint send statistics with duration
+* *Failures* (key *4*) -- recent exchange failures with exception type and 
message
+* *Redeliveries* (key *5*) -- recent redelivery attempts with attempt and max 
counts
+
+Press *Enter* on a route to drill down into its processors filtered by that 
route.
+Press *s* to cycle sort columns, and *Esc* to go back.
+
+Each snapshot is a point-in-time read -- the recording keeps running, so you 
can press *F5*
+again at any time to see updated statistics as more messages flow through the 
system.
+
 === Catalog
 
 The Catalog tab (under More > Project) lets you browse the full Camel 
component catalog from within
diff --git 
a/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
 
b/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
index b1e4a9507fea..c5859350123f 100644
--- 
a/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
+++ 
b/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
@@ -1012,6 +1012,14 @@ public class LocalCliConnector extends ServiceSupport 
implements CliConnector, C
             if (disable != null) {
                 params.put("disable", disable);
             }
+            String routeId = root.getString("routeId");
+            if (routeId != null) {
+                params.put("routeId", routeId);
+            }
+            String limit = root.getString("limit");
+            if (limit != null) {
+                params.put("limit", limit);
+            }
             JsonObject json = (JsonObject) dc.call(DevConsole.MediaType.JSON, 
params);
             LOG.trace("Updating output file: {}", outputFile);
             IOHelper.writeText(json.toJson(), outputFile);
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/JfrTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/JfrTab.java
index ba0b46104c5f..6fa628aeed13 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/JfrTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/JfrTab.java
@@ -17,12 +17,14 @@
 package org.apache.camel.dsl.jbang.core.commands.tui;
 
 import java.util.ArrayList;
-import java.util.LinkedHashMap;
+import java.util.Comparator;
 import java.util.List;
-import java.util.Map;
+import java.util.Locale;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.function.Consumer;
 
+import dev.tamboui.layout.Constraint;
+import dev.tamboui.layout.Layout;
 import dev.tamboui.layout.Rect;
 import dev.tamboui.style.Style;
 import dev.tamboui.terminal.Frame;
@@ -31,10 +33,16 @@ 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.tui.event.MouseEvent;
 import dev.tamboui.widgets.block.Block;
 import dev.tamboui.widgets.block.BorderType;
 import dev.tamboui.widgets.block.Borders;
 import dev.tamboui.widgets.paragraph.Paragraph;
+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 org.apache.camel.util.json.JsonArray;
 import org.apache.camel.util.json.JsonObject;
 
@@ -43,17 +51,80 @@ import static 
org.apache.camel.dsl.jbang.core.commands.tui.TuiHelper.*;
 class JfrTab extends AbstractTab {
 
     private static final Style LABEL = Theme.muted();
-    private static final Style OK = Style.EMPTY.fg(Theme.baseFg()).bold();
+    private static final Style VALUE = Style.EMPTY.fg(Theme.baseFg()).bold();
+
+    enum View {
+        ROUTES("Routes"),
+        PROCESSORS("Processors"),
+        ENDPOINTS("Endpoints"),
+        FAILURES("Failures"),
+        REDELIVERIES("Redeliveries");
+
+        final String label;
+
+        View(String label) {
+            this.label = label;
+        }
+    }
 
     private final AtomicBoolean loading = new AtomicBoolean(false);
     private final Consumer<Runnable> renderThreadExecutor;
 
+    // status state
     private boolean registered;
     private List<String> recordings = List.of();
-    private Map<String, Boolean> events = new LinkedHashMap<>();
-    private String message;
     private String errorMessage;
-    private boolean dataLoaded;
+    private boolean statusLoaded;
+
+    // snapshot data
+    private List<RouteStats> routeData = List.of();
+    private List<ProcessorStats> processorData = List.of();
+    private List<EndpointStats> endpointData = List.of();
+    private List<FailureEntry> failureData = List.of();
+    private List<RedeliveryEntry> redeliveryData = List.of();
+    private int snapshotEventCount;
+    private long snapshotTime;
+    private boolean snapshotLoaded;
+
+    // view state
+    private View activeView = View.ROUTES;
+    private final TableState tableState = new TableState();
+    private final ScrollbarState scrollState = new ScrollbarState();
+    private final TableState processorTableState = new TableState();
+    private final ScrollbarState processorScrollState = new ScrollbarState();
+    private Rect lastTableArea;
+    private int topPanelHeight = -1;
+
+    // sort state per view
+    private static final String[] ROUTE_SORT_COLUMNS = { "route", "total", 
"failed", "min", "mean", "max" };
+    private static final String[] PROCESSOR_SORT_COLUMNS = { "processor", 
"total", "failed", "min", "mean", "max" };
+    private static final String[] ENDPOINT_SORT_COLUMNS = { "endpoint", 
"total", "failed", "min", "mean", "max" };
+    private String routeSort = "route";
+    private int routeSortIndex;
+    private boolean routeSortReversed;
+    private String processorSort = "processor";
+    private int processorSortIndex;
+    private boolean processorSortReversed;
+    private String endpointSort = "endpoint";
+    private int endpointSortIndex;
+    private boolean endpointSortReversed;
+
+    record RouteStats(String routeId, long total, long failed, double minMs, 
double meanMs, double maxMs) {
+    }
+
+    record ProcessorStats(String processorId, String processorType, String 
routeId,
+            long total, long failed, double minMs, double meanMs, double 
maxMs) {
+    }
+
+    record EndpointStats(String endpointUri, long total, long failed, double 
minMs, double meanMs, double maxMs) {
+    }
+
+    record FailureEntry(String timestamp, String exchangeId, String routeId,
+            String exceptionType, String exceptionMessage) {
+    }
+
+    record RedeliveryEntry(String timestamp, String exchangeId, String 
routeId, int attempt, int maxAttempts) {
+    }
 
     JfrTab(MonitorContext ctx) {
         this(ctx, action -> {
@@ -70,7 +141,7 @@ class JfrTab extends AbstractTab {
 
     @Override
     public void onTabSelected() {
-        if (!dataLoaded) {
+        if (!statusLoaded) {
             refreshStatus();
         }
     }
@@ -79,33 +150,110 @@ class JfrTab extends AbstractTab {
     public void onIntegrationChanged() {
         registered = false;
         recordings = List.of();
-        events = new LinkedHashMap<>();
-        message = null;
         errorMessage = null;
-        dataLoaded = false;
+        statusLoaded = false;
+        clearSnapshotData();
+    }
+
+    private void clearSnapshotData() {
+        routeData = List.of();
+        processorData = List.of();
+        endpointData = List.of();
+        failureData = List.of();
+        redeliveryData = List.of();
+        snapshotEventCount = 0;
+        snapshotLoaded = false;
+        topPanelHeight = -1;
+        tableState.select(0);
+        processorTableState.select(0);
     }
 
     @Override
     public boolean handleKeyEvent(KeyEvent ke) {
         if (ke.isKey(KeyCode.F5)) {
-            refreshStatus();
+            takeSnapshot();
             return true;
         }
-        if (ke.isCharIgnoreCase('e')) {
-            toggleAll(true);
+        if (ke.isChar(' ')) {
+            View[] views = View.values();
+            switchView(views[(activeView.ordinal() + 1) % views.length]);
             return true;
         }
-        if (ke.isCharIgnoreCase('d')) {
-            toggleAll(false);
+
+        if (ke.isChar('s')) {
+            cycleSortForward();
             return true;
         }
-        if (ke.isCharIgnoreCase('j')) {
-            generateJfc();
+        if (ke.isChar('S')) {
+            cycleSortReverse();
+            return true;
+        }
+
+        if (ke.isPageUp() || ke.isKey(KeyCode.PAGE_UP)) {
+            TableState ts = activeTableState();
+            for (int i = 0; i < 20 && ts.selected() != null && ts.selected() > 
0; i++) {
+                ts.selectPrevious();
+            }
             return true;
         }
+        if (ke.isPageDown() || ke.isKey(KeyCode.PAGE_DOWN)) {
+            TableState ts = activeTableState();
+            int count = getRowCount();
+            for (int i = 0; i < 20; i++) {
+                ts.selectNext(count);
+            }
+            return true;
+        }
+        if (ke.isHome()) {
+            activeTableState().selectFirst();
+            return true;
+        }
+        if (ke.isEnd()) {
+            activeTableState().selectLast(getRowCount());
+            return true;
+        }
+
         return false;
     }
 
+    @Override
+    public void navigateUp() {
+        activeTableState().selectPrevious();
+    }
+
+    @Override
+    public void navigateDown() {
+        activeTableState().selectNext(getRowCount());
+    }
+
+    @Override
+    public boolean handleMouseEvent(MouseEvent me, Rect area) {
+        return handleTableClick(me, lastTableArea, activeTableState(), 
getRowCount());
+    }
+
+    private TableState activeTableState() {
+        return tableState;
+    }
+
+    private void switchView(View view) {
+        if (view != activeView) {
+            activeView = view;
+            tableState.select(0);
+        }
+    }
+
+    private int getRowCount() {
+        return switch (activeView) {
+            case ROUTES -> routeData.size();
+            case PROCESSORS -> processorData.size();
+            case ENDPOINTS -> endpointData.size();
+            case FAILURES -> failureData.size();
+            case REDELIVERIES -> redeliveryData.size();
+        };
+    }
+
+    // ---- rendering ----
+
     @Override
     public void render(Frame frame, Rect area) {
         IntegrationInfo info = ctx.findSelectedIntegration();
@@ -114,101 +262,549 @@ class JfrTab extends AbstractTab {
             return;
         }
 
-        if (loading.get() && !dataLoaded) {
+        List<Rect> rows = Layout.vertical()
+                .constraints(Constraint.length(4), Constraint.fill())
+                .split(area);
+
+        renderStatusHeader(frame, rows.get(0));
+        renderDataTable(frame, rows.get(1));
+    }
+
+    private void renderStatusHeader(Frame frame, Rect area) {
+        List<Line> lines = new ArrayList<>();
+
+        List<Span> line1 = new ArrayList<>();
+        if (!statusLoaded) {
+            line1.add(Span.styled("Loading...", LABEL));
+        } else {
+            line1.add(Span.styled("Runtime Events: ", LABEL));
+            line1.add(Span.styled(registered ? "Enabled" : "Disabled", 
registered ? VALUE : Theme.error()));
+
+            if (!recordings.isEmpty()) {
+                line1.add(Span.styled("  Recording: ", LABEL));
+                line1.add(Span.styled(recordings.get(0), Theme.success()));
+            } else {
+                line1.add(Span.styled("  No Active Recording", LABEL));
+            }
+        }
+
+        if (snapshotLoaded) {
+            line1.add(Span.styled("  Snapshot: ", LABEL));
+            String ageLabel = "";
+            if (snapshotTime > 0) {
+                long agoSec = (System.currentTimeMillis() - snapshotTime) / 
1000;
+                if (agoSec >= 60) {
+                    ageLabel = " (" + (agoSec / 60) + "m ago)";
+                } else if (agoSec >= 5) {
+                    ageLabel = " (" + agoSec + "s ago)";
+                }
+            }
+            line1.add(Span.styled(snapshotEventCount + " events" + ageLabel, 
VALUE));
+        }
+        lines.add(Line.from(line1));
+
+        List<Span> line2 = new ArrayList<>();
+        for (View v : View.values()) {
+            if (v.ordinal() > 0) {
+                line2.add(Span.styled(" | ", LABEL));
+            }
+            boolean active = v == activeView;
+            line2.add(Span.styled(v.label, active ? VALUE : LABEL));
+        }
+        if (errorMessage != null) {
+            line2.add(Span.styled("  " + errorMessage, Theme.error()));
+        }
+        lines.add(Line.from(line2));
+
+        frame.renderWidget(
+                Paragraph.builder().text(Text.from(lines))
+                        
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
+                                .title(" JFR ").build())
+                        .build(),
+                area);
+    }
+
+    private void renderDataTable(Frame frame, Rect area) {
+        if (!snapshotLoaded) {
+            String msg = loading.get()
+                    ? "  Loading..."
+                    : "  Press F5 to refresh JFR snapshot data";
             frame.renderWidget(
                     Paragraph.builder()
-                            .text(Text.from(Line.from(Span.styled("  Loading 
JFR status...", LABEL))))
+                            .text(Text.from(Line.from(Span.styled(msg, 
LABEL))))
                             
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
-                                    .title(" JFR Runtime Instrumentation 
").build())
+                                    .title(" " + activeView.label + " 
").build())
                             .build(),
                     area);
             return;
         }
 
-        if (errorMessage != null && !dataLoaded) {
+        switch (activeView) {
+            case ROUTES -> renderRoutesTable(frame, area);
+            case PROCESSORS -> renderProcessorsTable(frame, area);
+            case ENDPOINTS -> renderEndpointsTable(frame, area);
+            case FAILURES -> renderFailuresTable(frame, area);
+            case REDELIVERIES -> renderRedeliveriesTable(frame, area);
+        }
+    }
+
+    private void renderRoutesTable(Frame frame, Rect area) {
+        List<RouteStats> data = sortedRoutes();
+
+        if (topPanelHeight < 0) {
+            topPanelHeight = area.height() * 45 / 100;
+        }
+        topPanelHeight = Math.max(3, Math.min(topPanelHeight, area.height() - 
5));
+        List<Rect> chunks = Layout.vertical()
+                .constraints(Constraint.length(topPanelHeight), 
Constraint.fill())
+                .split(area);
+
+        long maxMean = data.stream().mapToLong(r -> (long) 
r.meanMs).max().orElse(1);
+        if (maxMean <= 0) {
+            maxMean = 1;
+        }
+
+        List<Row> rows = new ArrayList<>();
+        for (RouteStats r : data) {
+            Style nameStyle = r.failed > 0 ? Theme.error() : 
Style.EMPTY.fg(Theme.accent());
+            String bar = buildBar((long) r.meanMs, maxMean, 20);
+            Style barStyle = topTimeStyle((long) r.meanMs);
+            if (barStyle == Style.EMPTY) {
+                barStyle = Style.EMPTY.fg(Theme.accent());
+            }
+            rows.add(Row.from(
+                    Cell.from(Span.styled(r.routeId, nameStyle)),
+                    Cell.from(Span.styled(bar, barStyle)),
+                    rightCell(String.valueOf(r.total), 8),
+                    rightCell(String.valueOf(r.failed), 8, r.failed > 0 ? 
Theme.error() : Style.EMPTY),
+                    rightCell(formatMs(r.minMs), 10),
+                    rightCell(formatMs(r.meanMs), 10, topTimeStyle((long) 
r.meanMs)),
+                    rightCell(formatMs(r.maxMs), 10, topTimeStyle((long) 
r.maxMs))));
+        }
+
+        Table table = Table.builder()
+                .rows(rows)
+                .header(Row.from(
+                        Cell.from(Span.styled(routeSortLabel("ROUTE", 
"route"), routeSortStyle("route"))),
+                        Cell.from(""),
+                        rightCell(routeSortLabel("TOTAL", "total"), 8, 
routeSortStyle("total")),
+                        rightCell(routeSortLabel("FAILED", "failed"), 8, 
routeSortStyle("failed")),
+                        rightCell(routeSortLabel("MIN", "min"), 10, 
routeSortStyle("min")),
+                        rightCell(routeSortLabel("MEAN", "mean"), 10, 
routeSortStyle("mean")),
+                        rightCell(routeSortLabel("MAX", "max"), 10, 
routeSortStyle("max"))))
+                .widths(Constraint.length(24), Constraint.fill(),
+                        Constraint.length(8), Constraint.length(8),
+                        Constraint.length(10), Constraint.length(10), 
Constraint.length(10))
+                .highlightStyle(Theme.selectionBg())
+                .highlightSpacing(Table.HighlightSpacing.ALWAYS)
+                
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
+                        .title(" Routes (" + data.size() + ") ").build())
+                .build();
+        lastTableArea = chunks.get(0);
+        frame.renderStatefulWidget(table, chunks.get(0), tableState);
+        renderTableScrollbar(frame, lastTableArea, tableState, scrollState, 
data.size());
+
+        Integer selectedRoute = tableState.selected();
+        if (selectedRoute != null && selectedRoute >= 0 && selectedRoute < 
data.size()) {
+            RouteStats route = data.get(selectedRoute);
+            renderProcessorPanel(frame, chunks.get(1), route.routeId);
+        } else if (!data.isEmpty()) {
+            renderProcessorPanel(frame, chunks.get(1), data.get(0).routeId);
+        } else {
             frame.renderWidget(
                     Paragraph.builder()
-                            .text(Text.from(Line.from(Span.styled("  " + 
errorMessage, Theme.error()))))
+                            .text(Text.from(Line.from(Span.styled("No routes", 
Style.EMPTY.dim()))))
                             
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
-                                    .title(" JFR Runtime Instrumentation 
").build())
+                                    .title(" Processors ").build())
                             .build(),
-                    area);
-            return;
+                    chunks.get(1));
         }
+    }
 
-        List<Line> lines = new ArrayList<>();
-        lines.add(Line.from(
-                Span.styled("  registered: ", LABEL),
-                Span.styled(String.valueOf(registered), registered ? OK : 
Theme.error())));
-        lines.add(Line.from(Span.raw("")));
-        if (recordings.isEmpty()) {
-            lines.add(Line.from(Span.styled("  no active recording", LABEL)));
-        } else {
-            for (String recording : recordings) {
-                lines.add(Line.from(Span.styled("  recording: ", LABEL), 
Span.styled(recording, OK)));
+    private void renderProcessorPanel(Frame frame, Rect area, String routeId) {
+        List<ProcessorStats> data = processorData.stream()
+                .filter(p -> routeId.equals(p.routeId))
+                .toList();
+        data = sortProcessors(data);
+
+        long maxMean = data.stream().mapToLong(p -> (long) 
p.meanMs).max().orElse(1);
+        if (maxMean <= 0) {
+            maxMean = 1;
+        }
+
+        List<Row> rows = new ArrayList<>();
+        for (ProcessorStats p : data) {
+            Style nameStyle = p.failed > 0 ? Theme.error() : 
Style.EMPTY.fg(Theme.accent());
+            String bar = buildBar((long) p.meanMs, maxMean, 20);
+            Style barStyle = topTimeStyle((long) p.meanMs);
+            if (barStyle == Style.EMPTY) {
+                barStyle = Style.EMPTY.fg(Theme.accent());
             }
+            rows.add(Row.from(
+                    Cell.from(Span.styled(p.processorId, nameStyle)),
+                    Cell.from(Span.styled(p.processorType != null ? 
p.processorType : "", LABEL)),
+                    Cell.from(Span.styled(bar, barStyle)),
+                    rightCell(String.valueOf(p.total), 8),
+                    rightCell(String.valueOf(p.failed), 8, p.failed > 0 ? 
Theme.error() : Style.EMPTY),
+                    rightCell(formatMs(p.minMs), 10),
+                    rightCell(formatMs(p.meanMs), 10, topTimeStyle((long) 
p.meanMs)),
+                    rightCell(formatMs(p.maxMs), 10, topTimeStyle((long) 
p.maxMs))));
         }
-        lines.add(Line.from(Span.raw("")));
-        for (Map.Entry<String, Boolean> entry : events.entrySet()) {
-            boolean enabled = Boolean.TRUE.equals(entry.getValue());
-            lines.add(Line.from(
-                    Span.styled("  " + entry.getKey() + ": ", LABEL),
-                    Span.styled(enabled ? "enabled" : "disabled", enabled ? OK 
: Theme.error())));
+
+        Table table = Table.builder()
+                .rows(rows)
+                .header(Row.from(
+                        Cell.from(Span.styled("PROCESSOR", 
Style.EMPTY.bold())),
+                        Cell.from(Span.styled("TYPE", Style.EMPTY.bold())),
+                        Cell.from(""),
+                        rightCell("TOTAL", 8, Style.EMPTY.bold()),
+                        rightCell("FAILED", 8, Style.EMPTY.bold()),
+                        rightCell("MIN", 10, Style.EMPTY.bold()),
+                        rightCell("MEAN", 10, Style.EMPTY.bold()),
+                        rightCell("MAX", 10, Style.EMPTY.bold())))
+                .widths(Constraint.fill(), Constraint.length(12), 
Constraint.length(22),
+                        Constraint.length(8), Constraint.length(8),
+                        Constraint.length(10), Constraint.length(10), 
Constraint.length(10))
+                .highlightStyle(Theme.selectionBg())
+                .highlightSpacing(Table.HighlightSpacing.ALWAYS)
+                
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
+                        .title(" Processors [" + routeId + "] (" + data.size() 
+ ") ").build())
+                .build();
+        frame.renderStatefulWidget(table, area, processorTableState);
+        renderTableScrollbar(frame, area, processorTableState, 
processorScrollState, data.size());
+    }
+
+    private void renderProcessorsTable(Frame frame, Rect area) {
+        List<ProcessorStats> data = sortProcessors(processorData);
+
+        long maxMean = data.stream().mapToLong(p -> (long) 
p.meanMs).max().orElse(1);
+        if (maxMean <= 0) {
+            maxMean = 1;
         }
-        if (message != null) {
-            lines.add(Line.from(Span.raw("")));
-            lines.add(Line.from(Span.styled("  " + message, LABEL)));
+
+        List<Row> rows = new ArrayList<>();
+        for (ProcessorStats p : data) {
+            Style nameStyle = p.failed > 0 ? Theme.error() : 
Style.EMPTY.fg(Theme.accent());
+            String bar = buildBar((long) p.meanMs, maxMean, 20);
+            Style barStyle = topTimeStyle((long) p.meanMs);
+            if (barStyle == Style.EMPTY) {
+                barStyle = Style.EMPTY.fg(Theme.accent());
+            }
+            rows.add(Row.from(
+                    Cell.from(Span.styled(p.processorId, nameStyle)),
+                    Cell.from(Span.styled(p.processorType != null ? 
p.processorType : "", LABEL)),
+                    Cell.from(Span.styled(p.routeId, LABEL)),
+                    Cell.from(Span.styled(bar, barStyle)),
+                    rightCell(String.valueOf(p.total), 8),
+                    rightCell(String.valueOf(p.failed), 8, p.failed > 0 ? 
Theme.error() : Style.EMPTY),
+                    rightCell(formatMs(p.minMs), 10),
+                    rightCell(formatMs(p.meanMs), 10, topTimeStyle((long) 
p.meanMs)),
+                    rightCell(formatMs(p.maxMs), 10, topTimeStyle((long) 
p.maxMs))));
         }
-        // an error raised after the status was loaded, so it is shown inline 
rather than replacing the whole pane
-        if (errorMessage != null) {
-            lines.add(Line.from(Span.raw("")));
-            lines.add(Line.from(Span.styled("  " + errorMessage, 
Theme.error())));
+
+        Table table = Table.builder()
+                .rows(rows)
+                .header(Row.from(
+                        Cell.from(Span.styled(processorSortLabel("PROCESSOR", 
"processor"),
+                                processorSortStyle("processor"))),
+                        Cell.from(Span.styled("TYPE", Style.EMPTY.bold())),
+                        Cell.from(Span.styled("ROUTE", Style.EMPTY.bold())),
+                        Cell.from(""),
+                        rightCell(processorSortLabel("TOTAL", "total"), 8, 
processorSortStyle("total")),
+                        rightCell(processorSortLabel("FAILED", "failed"), 8, 
processorSortStyle("failed")),
+                        rightCell(processorSortLabel("MIN", "min"), 10, 
processorSortStyle("min")),
+                        rightCell(processorSortLabel("MEAN", "mean"), 10, 
processorSortStyle("mean")),
+                        rightCell(processorSortLabel("MAX", "max"), 10, 
processorSortStyle("max"))))
+                .widths(Constraint.fill(), Constraint.length(12), 
Constraint.length(16),
+                        Constraint.length(22),
+                        Constraint.length(8), Constraint.length(8),
+                        Constraint.length(10), Constraint.length(10), 
Constraint.length(10))
+                .highlightStyle(Theme.selectionBg())
+                .highlightSpacing(Table.HighlightSpacing.ALWAYS)
+                
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
+                        .title(" Processors (" + data.size() + ") ").build())
+                .build();
+        lastTableArea = area;
+        frame.renderStatefulWidget(table, area, tableState);
+        renderTableScrollbar(frame, lastTableArea, tableState, scrollState, 
data.size());
+    }
+
+    private void renderEndpointsTable(Frame frame, Rect area) {
+        List<EndpointStats> data = sortedEndpoints();
+
+        long maxMean = data.stream().mapToLong(e -> (long) 
e.meanMs).max().orElse(1);
+        if (maxMean <= 0) {
+            maxMean = 1;
         }
 
-        frame.renderWidget(
-                Paragraph.builder()
-                        .text(Text.from(lines))
-                        
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
-                                .title(" JFR Runtime Instrumentation 
").build())
-                        .build(),
-                area);
+        List<Row> rows = new ArrayList<>();
+        for (EndpointStats e : data) {
+            Style nameStyle = e.failed > 0 ? Theme.error() : 
Style.EMPTY.fg(Theme.accent());
+            String bar = buildBar((long) e.meanMs, maxMean, 20);
+            Style barStyle = topTimeStyle((long) e.meanMs);
+            if (barStyle == Style.EMPTY) {
+                barStyle = Style.EMPTY.fg(Theme.accent());
+            }
+            rows.add(Row
+                    .from(
+                    Cell.from(Span.styled(e.endpointUri, nameStyle)),
+                    Cell.from(Span.styled(bar, barStyle)),
+                    rightCell(String.valueOf(e.total), 8),
+                    rightCell(String.valueOf(e.failed), 8, e.failed > 0 ? 
Theme.error() : Style.EMPTY),
+                    rightCell(formatMs(e.minMs), 10),
+                    rightCell(formatMs(e.meanMs), 10, topTimeStyle((long) 
e.meanMs)),
+                    rightCell(formatMs(e.maxMs), 10, topTimeStyle((long) 
e.maxMs))));
+        }
+
+        Table table = Table.builder()
+                .rows(rows)
+                .header(Row.from(
+                        Cell.from(Span.styled(endpointSortLabel("ENDPOINT", 
"endpoint"),
+                                endpointSortStyle("endpoint"))),
+                        Cell.from(""),
+                        rightCell(endpointSortLabel("TOTAL", "total"), 8, 
endpointSortStyle("total")),
+                        rightCell(endpointSortLabel("FAILED", "failed"), 8, 
endpointSortStyle("failed")),
+                        rightCell(endpointSortLabel("MIN", "min"), 10, 
endpointSortStyle("min")),
+                        rightCell(endpointSortLabel("MEAN", "mean"), 10, 
endpointSortStyle("mean")),
+                        rightCell(endpointSortLabel("MAX", "max"), 10, 
endpointSortStyle("max"))))
+                .widths(Constraint.length(30), Constraint.fill(),
+                        Constraint.length(8), Constraint.length(8),
+                        Constraint.length(10), Constraint.length(10), 
Constraint.length(10))
+                .highlightStyle(Theme.selectionBg())
+                .highlightSpacing(Table.HighlightSpacing.ALWAYS)
+                
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
+                        .title(" Endpoints (" + data.size() + ") ").build())
+                .build();
+        lastTableArea = area;
+        frame.renderStatefulWidget(table, area, tableState);
+        renderTableScrollbar(frame, lastTableArea, tableState, scrollState, 
data.size());
+    }
+
+    private void renderFailuresTable(Frame frame, Rect area) {
+        List<Row> rows = new ArrayList<>();
+        for (FailureEntry f : failureData) {
+            String time = f.timestamp != null && f.timestamp.length() > 19
+                    ? f.timestamp.substring(11, 19) : (f.timestamp != null ? 
f.timestamp : "");
+            String msg = f.exceptionMessage != null
+                    ? (f.exceptionMessage.length() > 60 ? 
f.exceptionMessage.substring(0, 60) + "..." : f.exceptionMessage)
+                    : "";
+            rows.add(Row.from(
+                    Cell.from(Span.styled(time, LABEL)),
+                    Cell.from(Span.styled(f.routeId != null ? f.routeId : "", 
Style.EMPTY.fg(Theme.baseFg()))),
+                    Cell.from(Span.styled(
+                            f.exceptionType != null ? 
shortClassName(f.exceptionType) : "", Theme.error())),
+                    Cell.from(Span.styled(msg, LABEL))));
+        }
+
+        Style hdr = Style.EMPTY.bold();
+        Table table = Table.builder()
+                .rows(rows)
+                .header(Row.from(
+                        Cell.from(Span.styled("TIME", hdr)),
+                        Cell.from(Span.styled("ROUTE", hdr)),
+                        Cell.from(Span.styled("EXCEPTION", hdr)),
+                        Cell.from(Span.styled("MESSAGE", hdr))))
+                .widths(Constraint.length(10), Constraint.length(16),
+                        Constraint.length(28), Constraint.fill())
+                .highlightStyle(Theme.selectionBg())
+                .highlightSpacing(Table.HighlightSpacing.ALWAYS)
+                
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
+                        .title(" Failures (" + failureData.size() + ") 
").build())
+                .build();
+        lastTableArea = area;
+        frame.renderStatefulWidget(table, area, tableState);
+        renderTableScrollbar(frame, lastTableArea, tableState, scrollState, 
failureData.size());
+    }
+
+    private void renderRedeliveriesTable(Frame frame, Rect area) {
+        List<Row> rows = new ArrayList<>();
+        for (RedeliveryEntry r : redeliveryData) {
+            String time = r.timestamp != null && r.timestamp.length() > 19
+                    ? r.timestamp.substring(11, 19) : (r.timestamp != null ? 
r.timestamp : "");
+            boolean exhausted = r.attempt >= r.maxAttempts && r.maxAttempts > 
0;
+            rows.add(Row.from(
+                    Cell.from(Span.styled(time, LABEL)),
+                    Cell.from(Span.styled(r.routeId != null ? r.routeId : "", 
Style.EMPTY.fg(Theme.baseFg()))),
+                    rightCell(String.valueOf(r.attempt), 8),
+                    rightCell(String.valueOf(r.maxAttempts), 8, exhausted ? 
Theme.error() : Style.EMPTY)));
+        }
+
+        Style hdr = Style.EMPTY.bold();
+        Table table = Table.builder()
+                .rows(rows)
+                .header(Row.from(
+                        Cell.from(Span.styled("TIME", hdr)),
+                        Cell.from(Span.styled("ROUTE", hdr)),
+                        rightCell("ATTEMPT", 8, hdr),
+                        rightCell("MAX", 8, hdr)))
+                .widths(Constraint.length(10), Constraint.fill(),
+                        Constraint.length(8), Constraint.length(8))
+                .highlightStyle(Theme.selectionBg())
+                .highlightSpacing(Table.HighlightSpacing.ALWAYS)
+                
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
+                        .title(" Redeliveries (" + redeliveryData.size() + ") 
").build())
+                .build();
+        lastTableArea = area;
+        frame.renderStatefulWidget(table, area, tableState);
+        renderTableScrollbar(frame, lastTableArea, tableState, scrollState, 
redeliveryData.size());
+    }
+
+    private static String formatMs(double ms) {
+        if (ms < 1.0) {
+            return String.format(Locale.US, "%.1fms", ms);
+        } else if (ms < 1000.0) {
+            return String.format(Locale.US, "%.0fms", ms);
+        } else {
+            return String.format(Locale.US, "%.1fs", ms / 1000.0);
+        }
+    }
+
+    private static String shortClassName(String fqcn) {
+        int dot = fqcn.lastIndexOf('.');
+        return dot >= 0 ? fqcn.substring(dot + 1) : fqcn;
     }
 
     @Override
     public void renderFooter(List<Span> spans) {
         hint(spans, "Esc", "back");
-        hint(spans, "E", "enable all");
-        hint(spans, "D", "disable all");
-        hint(spans, "J", "generate .jfc");
+        hint(spans, "Space", "view");
+        if (snapshotLoaded && hasSortColumns()) {
+            hint(spans, "s", "sort");
+        }
         hintLast(spans, "F5", "refresh");
     }
 
-    private void refreshStatus() {
-        if (ctx.selectedPid == null) {
-            return;
+    // ---- sorting ----
+
+    private void cycleSortForward() {
+        switch (activeView) {
+            case ROUTES -> {
+                routeSortIndex = (routeSortIndex + 1) % 
ROUTE_SORT_COLUMNS.length;
+                routeSort = ROUTE_SORT_COLUMNS[routeSortIndex];
+                routeSortReversed = false;
+            }
+            case PROCESSORS -> {
+                processorSortIndex = (processorSortIndex + 1) % 
PROCESSOR_SORT_COLUMNS.length;
+                processorSort = PROCESSOR_SORT_COLUMNS[processorSortIndex];
+                processorSortReversed = false;
+            }
+            case ENDPOINTS -> {
+                endpointSortIndex = (endpointSortIndex + 1) % 
ENDPOINT_SORT_COLUMNS.length;
+                endpointSort = ENDPOINT_SORT_COLUMNS[endpointSortIndex];
+                endpointSortReversed = false;
+            }
+            default -> {
+            }
         }
-        if (!loading.compareAndSet(false, true)) {
-            return;
+    }
+
+    private void cycleSortReverse() {
+        switch (activeView) {
+            case ROUTES -> routeSortReversed = !routeSortReversed;
+            case PROCESSORS -> processorSortReversed = !processorSortReversed;
+            case ENDPOINTS -> endpointSortReversed = !endpointSortReversed;
+            default -> {
+            }
         }
+    }
 
-        String pid = ctx.selectedPid;
-        ctx.backgroundExecutor.execute(() -> {
-            try {
-                JsonObject root = new JsonObject();
-                root.put("action", "jfr");
-                root.put("command", "status");
+    private boolean hasSortColumns() {
+        return activeView == View.ROUTES || activeView == View.PROCESSORS || 
activeView == View.ENDPOINTS;
+    }
 
-                JsonObject jo = ctx.executeAction(pid, root, 5000);
-                applyStatus(jo);
-            } catch (Exception e) {
-                applyError("Error: " + e.getMessage());
-            } finally {
-                loading.set(false);
-            }
-        });
+    private String routeSortLabel(String label, String column) {
+        return sortLabel(label, column, routeSort, routeSortReversed);
+    }
+
+    private Style routeSortStyle(String column) {
+        return sortStyle(column, routeSort);
+    }
+
+    private String processorSortLabel(String label, String column) {
+        return sortLabel(label, column, processorSort, processorSortReversed);
+    }
+
+    private Style processorSortStyle(String column) {
+        return sortStyle(column, processorSort);
+    }
+
+    private String endpointSortLabel(String label, String column) {
+        return sortLabel(label, column, endpointSort, endpointSortReversed);
+    }
+
+    private Style endpointSortStyle(String column) {
+        return sortStyle(column, endpointSort);
     }
 
-    private void toggleAll(boolean enable) {
+    private List<RouteStats> sortedRoutes() {
+        if (routeData.isEmpty()) {
+            return routeData;
+        }
+        List<RouteStats> sorted = new ArrayList<>(routeData);
+        if ("route".equals(routeSort)) {
+            sorted.sort(routeSortReversed
+                    ? Comparator.comparing(RouteStats::routeId, 
String.CASE_INSENSITIVE_ORDER).reversed()
+                    : Comparator.comparing(RouteStats::routeId, 
String.CASE_INSENSITIVE_ORDER));
+        } else {
+            Comparator<RouteStats> cmp = switch (routeSort) {
+                case "failed" -> Comparator.comparingLong(RouteStats::failed);
+                case "min" -> Comparator.comparingDouble(RouteStats::minMs);
+                case "mean" -> Comparator.comparingDouble(RouteStats::meanMs);
+                case "max" -> Comparator.comparingDouble(RouteStats::maxMs);
+                default -> Comparator.comparingLong(RouteStats::total);
+            };
+            sorted.sort(routeSortReversed ? cmp : cmp.reversed());
+        }
+        return sorted;
+    }
+
+    private List<ProcessorStats> sortProcessors(List<ProcessorStats> input) {
+        if (input.isEmpty()) {
+            return input;
+        }
+        List<ProcessorStats> sorted = new ArrayList<>(input);
+        if ("processor".equals(processorSort)) {
+            sorted.sort(processorSortReversed
+                    ? Comparator.comparing(ProcessorStats::processorId, 
String.CASE_INSENSITIVE_ORDER).reversed()
+                    : Comparator.comparing(ProcessorStats::processorId, 
String.CASE_INSENSITIVE_ORDER));
+        } else {
+            Comparator<ProcessorStats> cmp = switch (processorSort) {
+                case "total" -> 
Comparator.comparingLong(ProcessorStats::total);
+                case "failed" -> 
Comparator.comparingLong(ProcessorStats::failed);
+                case "min" -> 
Comparator.comparingDouble(ProcessorStats::minMs);
+                case "max" -> 
Comparator.comparingDouble(ProcessorStats::maxMs);
+                default -> Comparator.comparingDouble(ProcessorStats::meanMs);
+            };
+            sorted.sort(processorSortReversed ? cmp : cmp.reversed());
+        }
+        return sorted;
+    }
+
+    private List<EndpointStats> sortedEndpoints() {
+        if (endpointData.isEmpty()) {
+            return endpointData;
+        }
+        List<EndpointStats> sorted = new ArrayList<>(endpointData);
+        if ("endpoint".equals(endpointSort)) {
+            sorted.sort(endpointSortReversed
+                    ? Comparator.comparing(EndpointStats::endpointUri, 
String.CASE_INSENSITIVE_ORDER).reversed()
+                    : Comparator.comparing(EndpointStats::endpointUri, 
String.CASE_INSENSITIVE_ORDER));
+        } else {
+            Comparator<EndpointStats> cmp = switch (endpointSort) {
+                case "failed" -> 
Comparator.comparingLong(EndpointStats::failed);
+                case "min" -> Comparator.comparingDouble(EndpointStats::minMs);
+                case "mean" -> 
Comparator.comparingDouble(EndpointStats::meanMs);
+                case "max" -> Comparator.comparingDouble(EndpointStats::maxMs);
+                default -> Comparator.comparingLong(EndpointStats::total);
+            };
+            sorted.sort(endpointSortReversed ? cmp : cmp.reversed());
+        }
+        return sorted;
+    }
+
+    // ---- server communication ----
+
+    private void refreshStatus() {
         if (ctx.selectedPid == null) {
             return;
         }
@@ -218,131 +814,358 @@ class JfrTab extends AbstractTab {
 
         String pid = ctx.selectedPid;
         ctx.backgroundExecutor.execute(() -> {
+            boolean chained = false;
             try {
                 JsonObject root = new JsonObject();
                 root.put("action", "jfr");
-                root.put("command", enable ? "enable" : "disable");
-                root.put("event", "all");
+                root.put("command", "status");
 
                 JsonObject jo = ctx.executeAction(pid, root, 5000);
-                String result = jo != null ? jo.getString("result") : null;
-                if (result == null) {
-                    applyError(errorOf(jo, "No response from integration"));
+                applyStatus(jo);
+
+                if (jo != null && jo.get("recordings") instanceof JsonArray 
recs && !recs.isEmpty()
+                        && !snapshotLoaded) {
+                    chained = true;
+                    doTakeSnapshot();
                     return;
                 }
-                if (Boolean.TRUE.equals(jo.getBoolean("success"))) {
-                    applyMessage(result);
-                } else {
-                    applyError(result);
-                }
             } catch (Exception e) {
                 applyError("Error: " + e.getMessage());
-                return;
             } finally {
-                loading.set(false);
+                if (!chained) {
+                    loading.set(false);
+                }
             }
-            // only refresh once the loading flag is released, as 
refreshStatus takes it again
-            refreshStatus();
         });
     }
 
-    private void generateJfc() {
+    private void takeSnapshot() {
         if (ctx.selectedPid == null) {
             return;
         }
         if (!loading.compareAndSet(false, true)) {
             return;
         }
+        doTakeSnapshot();
+    }
 
+    private void doTakeSnapshot() {
         String pid = ctx.selectedPid;
         ctx.backgroundExecutor.execute(() -> {
             try {
                 JsonObject root = new JsonObject();
                 root.put("action", "jfr");
-                root.put("command", "jfc");
+                root.put("command", "snapshot");
 
-                JsonObject jo = ctx.executeAction(pid, root, 5000);
-                String jfc = jo != null ? jo.getString("jfc") : null;
-                if (jfc == null) {
-                    applyError(errorOf(jo, "No response from integration"));
-                } else if (ctx.openMarkdownCallback != null && ctx.runner != 
null) {
-                    ctx.runner.runOnRenderThread(
-                            () -> ctx.openMarkdownCallback.accept("JFR .jfc 
overlay", "```\n" + jfc + "\n```"));
-                } else {
-                    applyMessage(jfc);
-                }
+                JsonObject jo = ctx.executeAction(pid, root, 30000);
+                applySnapshot(jo);
             } catch (Exception e) {
-                applyError("Error: " + e.getMessage());
+                applyError("Error taking snapshot: " + e.getMessage());
             } finally {
                 loading.set(false);
             }
         });
     }
 
+    // ---- apply results on render thread ----
+
     private void applyStatus(JsonObject jo) {
         renderThreadExecutor.accept(() -> {
             if (jo == null) {
                 errorMessage = "No response from integration";
-                dataLoaded = true;
+                statusLoaded = true;
                 return;
             }
             String error = jo.getString("error");
             if (error != null) {
                 errorMessage = error;
-                dataLoaded = true;
+                statusLoaded = true;
                 return;
             }
-            registered = Boolean.TRUE.equals(jo.getBoolean("registered"));
+            registered = Boolean.TRUE.equals(jo.getBoolean("runtimeEvents"));
             List<String> recs = new ArrayList<>();
             if (jo.get("recordings") instanceof JsonArray recordingsArr) {
                 for (Object o : recordingsArr) {
                     if (o instanceof JsonObject rec) {
-                        recs.add(rec.getString("name") + " (state=" + 
rec.getString("state") + ")");
+                        recs.add(rec.getString("name") + " (" + 
rec.getString("state") + ")");
                     }
                 }
             }
             recordings = recs;
-            Map<String, Boolean> evts = new LinkedHashMap<>();
-            if (jo.get("events") instanceof JsonObject eventsObj) {
-                eventsObj.forEach((k, v) -> evts.put(k, 
Boolean.TRUE.equals(v)));
-            }
-            events = evts;
             errorMessage = null;
-            dataLoaded = true;
+            statusLoaded = true;
         });
     }
 
-    /**
-     * The integration reports why it could not answer in an {@code error} 
field, which is more useful than the generic
-     * fallback.
-     */
-    private static String errorOf(JsonObject jo, String fallback) {
-        String error = jo != null ? jo.getString("error") : null;
-        return error != null ? error : fallback;
-    }
-
-    private void applyMessage(String msg) {
+    private void applySnapshot(JsonObject jo) {
         renderThreadExecutor.accept(() -> {
-            message = msg;
+            if (jo == null) {
+                errorMessage = "No snapshot response from integration";
+                return;
+            }
+            String error = jo.getString("error");
+            if (error != null) {
+                errorMessage = error;
+                return;
+            }
+
+            snapshotEventCount = jo.getInteger("eventCount") != null ? 
jo.getInteger("eventCount") : 0;
+
+            routeData = parseRoutes(jo);
+            processorData = parseProcessors(jo);
+            endpointData = parseEndpoints(jo);
+            failureData = parseFailures(jo);
+            redeliveryData = parseRedeliveries(jo);
+
             errorMessage = null;
+            Long ts = jo.getLong("snapshotTimestamp");
+            snapshotTime = ts != null ? ts : System.currentTimeMillis();
+            snapshotLoaded = true;
+            tableState.select(0);
         });
     }
 
+    private static List<RouteStats> parseRoutes(JsonObject jo) {
+        List<RouteStats> result = new ArrayList<>();
+        if (jo.get("routes") instanceof JsonArray arr) {
+            for (Object o : arr) {
+                if (o instanceof JsonObject r) {
+                    result.add(new RouteStats(
+                            r.getString("routeId"),
+                            longVal(r, "total"),
+                            longVal(r, "failed"),
+                            doubleVal(r, "minMs"),
+                            doubleVal(r, "meanMs"),
+                            doubleVal(r, "maxMs")));
+                }
+            }
+        }
+        return result;
+    }
+
+    private static List<ProcessorStats> parseProcessors(JsonObject jo) {
+        List<ProcessorStats> result = new ArrayList<>();
+        if (jo.get("processors") instanceof JsonArray arr) {
+            for (Object o : arr) {
+                if (o instanceof JsonObject p) {
+                    result.add(new ProcessorStats(
+                            p.getString("processorId"),
+                            p.getString("processorType"),
+                            p.getString("routeId"),
+                            longVal(p, "total"),
+                            longVal(p, "failed"),
+                            doubleVal(p, "minMs"),
+                            doubleVal(p, "meanMs"),
+                            doubleVal(p, "maxMs")));
+                }
+            }
+        }
+        return result;
+    }
+
+    private static List<EndpointStats> parseEndpoints(JsonObject jo) {
+        List<EndpointStats> result = new ArrayList<>();
+        if (jo.get("endpoints") instanceof JsonArray arr) {
+            for (Object o : arr) {
+                if (o instanceof JsonObject e) {
+                    result.add(new EndpointStats(
+                            e.getString("endpointUri"),
+                            longVal(e, "total"),
+                            longVal(e, "failed"),
+                            doubleVal(e, "minMs"),
+                            doubleVal(e, "meanMs"),
+                            doubleVal(e, "maxMs")));
+                }
+            }
+        }
+        return result;
+    }
+
+    private static List<FailureEntry> parseFailures(JsonObject jo) {
+        List<FailureEntry> result = new ArrayList<>();
+        if (jo.get("failures") instanceof JsonArray arr) {
+            for (Object o : arr) {
+                if (o instanceof JsonObject f) {
+                    result.add(new FailureEntry(
+                            f.getString("timestamp"),
+                            f.getString("exchangeId"),
+                            f.getString("routeId"),
+                            f.getString("exceptionType"),
+                            f.getString("exceptionMessage")));
+                }
+            }
+        }
+        return result;
+    }
+
+    private static List<RedeliveryEntry> parseRedeliveries(JsonObject jo) {
+        List<RedeliveryEntry> result = new ArrayList<>();
+        if (jo.get("redeliveries") instanceof JsonArray arr) {
+            for (Object o : arr) {
+                if (o instanceof JsonObject r) {
+                    result.add(new RedeliveryEntry(
+                            r.getString("timestamp"),
+                            r.getString("exchangeId"),
+                            r.getString("routeId"),
+                            intVal(r, "attempt"),
+                            intVal(r, "maxAttempts")));
+                }
+            }
+        }
+        return result;
+    }
+
+    private static long longVal(JsonObject jo, String key) {
+        Object v = jo.get(key);
+        if (v instanceof Number n) {
+            return n.longValue();
+        }
+        return 0;
+    }
+
+    private static double doubleVal(JsonObject jo, String key) {
+        Object v = jo.get(key);
+        if (v instanceof Number n) {
+            return n.doubleValue();
+        }
+        return 0;
+    }
+
+    private static int intVal(JsonObject jo, String key) {
+        Object v = jo.get(key);
+        if (v instanceof Number n) {
+            return n.intValue();
+        }
+        return 0;
+    }
+
     private void applyError(String error) {
         renderThreadExecutor.accept(() -> {
-            message = null;
             errorMessage = error;
         });
     }
 
     @Override
     public SelectionContext getSelectionContext() {
-        return null;
+        if (!snapshotLoaded) {
+            return null;
+        }
+        int sel = tableState.selected() != null ? tableState.selected() : -1;
+        return switch (activeView) {
+            case ROUTES -> {
+                List<RouteStats> data = sortedRoutes();
+                yield sel >= 0 && sel < data.size()
+                        ? new SelectionContext(
+                                "route",
+                                
data.stream().map(RouteStats::routeId).toList(),
+                                sel, data.size(), data.get(sel).routeId)
+                        : null;
+            }
+            case PROCESSORS -> {
+                List<ProcessorStats> data = sortProcessors(processorData);
+                yield sel >= 0 && sel < data.size()
+                        ? new SelectionContext(
+                                "processor",
+                                
data.stream().map(ProcessorStats::processorId).toList(),
+                                sel, data.size(), data.get(sel).processorId)
+                        : null;
+            }
+            case ENDPOINTS -> {
+                List<EndpointStats> data = sortedEndpoints();
+                yield sel >= 0 && sel < data.size()
+                        ? new SelectionContext(
+                                "endpoint",
+                                
data.stream().map(EndpointStats::endpointUri).toList(),
+                                sel, data.size(), data.get(sel).endpointUri)
+                        : null;
+            }
+            default -> null;
+        };
+    }
+
+    @Override
+    public JsonObject getTableDataAsJson() {
+        if (!snapshotLoaded) {
+            return null;
+        }
+        JsonObject result = new JsonObject();
+        result.put("tab", "JFR");
+        result.put("view", activeView.label);
+        result.put("eventCount", snapshotEventCount);
+
+        JsonArray rows = new JsonArray();
+        switch (activeView) {
+            case ROUTES -> {
+                for (RouteStats r : routeData) {
+                    JsonObject row = new JsonObject();
+                    row.put("routeId", r.routeId());
+                    row.put("total", r.total());
+                    row.put("failed", r.failed());
+                    row.put("minMs", r.minMs());
+                    row.put("meanMs", r.meanMs());
+                    row.put("maxMs", r.maxMs());
+                    rows.add(row);
+                }
+            }
+            case PROCESSORS -> {
+                for (ProcessorStats p : processorData) {
+                    JsonObject row = new JsonObject();
+                    row.put("processorId", p.processorId());
+                    row.put("processorType", p.processorType());
+                    row.put("routeId", p.routeId());
+                    row.put("total", p.total());
+                    row.put("failed", p.failed());
+                    row.put("minMs", p.minMs());
+                    row.put("meanMs", p.meanMs());
+                    row.put("maxMs", p.maxMs());
+                    rows.add(row);
+                }
+            }
+            case ENDPOINTS -> {
+                for (EndpointStats e : endpointData) {
+                    JsonObject row = new JsonObject();
+                    row.put("endpointUri", e.endpointUri());
+                    row.put("total", e.total());
+                    row.put("failed", e.failed());
+                    row.put("minMs", e.minMs());
+                    row.put("meanMs", e.meanMs());
+                    row.put("maxMs", e.maxMs());
+                    rows.add(row);
+                }
+            }
+            case FAILURES -> {
+                for (FailureEntry f : failureData) {
+                    JsonObject row = new JsonObject();
+                    row.put("timestamp", f.timestamp());
+                    row.put("exchangeId", f.exchangeId());
+                    row.put("routeId", f.routeId());
+                    row.put("exceptionType", f.exceptionType());
+                    row.put("exceptionMessage", f.exceptionMessage());
+                    rows.add(row);
+                }
+            }
+            case REDELIVERIES -> {
+                for (RedeliveryEntry r : redeliveryData) {
+                    JsonObject row = new JsonObject();
+                    row.put("timestamp", r.timestamp());
+                    row.put("exchangeId", r.exchangeId());
+                    row.put("routeId", r.routeId());
+                    row.put("attempt", r.attempt());
+                    row.put("maxAttempts", r.maxAttempts());
+                    rows.add(row);
+                }
+            }
+        }
+        result.put("rows", rows);
+        result.put("totalRows", rows.size());
+        int sel = tableState.selected() != null ? tableState.selected() : -1;
+        result.put("selectedIndex", sel);
+        return result;
     }
 
     @Override
     public String description() {
-        return "JFR runtime instrumentation status and live event toggling";
+        return "JFR runtime profiling with route, processor, and endpoint 
statistics";
     }
 
     @Override
@@ -350,21 +1173,30 @@ class JfrTab extends AbstractTab {
         return """
                 # JFR
 
-                The JFR tab shows the live status of camel-jfr's runtime 
instrumentation:
-                whether it is registered, any active Flight Recorder 
recording(s), and the
-                current enabled/disabled state of each of the six runtime 
events (route,
-                processor, exchange, send, failed, redelivery).
+                The JFR tab shows the live status of camel-jfr's runtime 
instrumentation
+                and lets you view aggregated runtime data from JFR recordings.
+
+                ## Data Views
+
+                Press **F5** to take a snapshot of the active JFR recording. 
The snapshot
+                data is aggregated into five views:
+
+                - **Routes** — per-route exchange count, failures, and timing 
with
+                  a processor panel below showing processors for the selected 
route
+                - **Processors** — per-processor invocation count and timing 
across all routes
+                - **Endpoints** — per-endpoint send count and timing
+                - **Failures** — recent exchange failures with exception 
details
+                - **Redeliveries** — recent redelivery attempts
 
-                ## Keys
+                ## Controls
 
-                - `E` — enable all runtime events on every active recording
-                - `D` — disable all runtime events on every active recording
-                - `J` — generate a `.jfc` overlay with the current event 
selection
-                - `F5` — refresh status
+                - `F5` — refresh (take new JFR snapshot)
+                - `Space` — cycle view
+                - `s` / `S` — cycle sort column / reverse sort direction
                 - `Esc` — back
 
-                Toggling requires at least one active recording; start one 
first via
-                `--jfr`, `jcmd <pid> JFR.start`, or JMX.
+                Requires at least one active recording; start one via `--jfr`,
+                `jcmd <pid> JFR.start`, or JMX.
                 """;
     }
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
index 80b783a1b2d2..c81494078da6 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
@@ -212,6 +212,9 @@ class TabRegistry {
                 new MoreTab(
                         TuiIcons.TAB_SPANS, "Spans", "&OTel Spans", spansTab, 
"Observability",
                         List.of("opentelemetry")),
+                new MoreTab(
+                        TuiIcons.TAB_JFR, "JFR", "J&FR", jfrTab, 
"Observability",
+                        List.of("jfr")),
                 // Data
                 new MoreTab(
                         TuiIcons.TAB_DATASOURCE, "JDBC DataSource", "&JDBC 
DataSource", dataSourceTab, "Data",
@@ -233,9 +236,6 @@ class TabRegistry {
                 // JVM
                 new MoreTab(TuiIcons.TAB_CLASSPATH, "Classpath", "&Classpath", 
classpathTab, "JVM"),
                 new MoreTab(TuiIcons.TAB_HEAP, "Heap Memory Histogram", "Heap 
&Memory Histogram", heapHistogramTab, "JVM"),
-                new MoreTab(
-                        TuiIcons.TAB_JFR, "Java Flight Recorder (JFR)", "Java 
Fli&ght Recorder (JFR)", jfrTab, "JVM",
-                        List.of("jfr")),
                 new MoreTab(TuiIcons.TAB_MEMORY, "Memory Usage", "&Memory 
Usage", memoryTab, "JVM"),
                 new MoreTab(TuiIcons.TAB_MEMORY_LEAK, "Memory Leak", "&Memory 
Leak", memoryLeakTab, "JVM"),
                 new MoreTab(TuiIcons.TAB_PROCESS, "Process", "&Process", 
processTab, "JVM"),
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/JfrTabRenderTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/JfrTabRenderTest.java
index 4e6ca7a5633f..96bb71294a66 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/JfrTabRenderTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/JfrTabRenderTest.java
@@ -22,6 +22,10 @@ import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicReference;
 
 import dev.tamboui.text.Span;
+import dev.tamboui.tui.event.KeyCode;
+import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.tui.event.KeyModifiers;
+import org.apache.camel.util.json.JsonArray;
 import org.apache.camel.util.json.JsonObject;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
@@ -29,10 +33,6 @@ import org.junit.jupiter.api.Test;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.awaitility.Awaitility.await;
 
-/**
- * Rendering tests for {@link JfrTab}. These tests render the tab into a 
virtual terminal buffer via
- * {@link Frame#forTesting(Buffer)} and inspect the rendered cell content.
- */
 class JfrTabRenderTest {
 
     private MonitorContext ctx;
@@ -70,7 +70,7 @@ class JfrTabRenderTest {
     void renderShowsRegisteredAndRecordingState() {
         JfrTab tab = new JfrTab(ctx);
         String rendered = TuiTestHelper.renderToString(tab, 120, 20);
-        assertThat(rendered).contains("registered").contains("no active 
recording");
+        assertThat(rendered).contains("Loading...");
     }
 
     @Test
@@ -95,6 +95,107 @@ class JfrTabRenderTest {
         assertThat(footer).contains("Esc").contains("F5").contains("refresh");
     }
 
+    @Test
+    void renderFooterShowsViewHint() {
+        JfrTab tab = new JfrTab(ctx);
+        List<Span> footerSpans = new ArrayList<>();
+        tab.renderFooter(footerSpans);
+        String footer = footerSpans.stream().map(Span::content).reduce("", 
String::concat);
+
+        assertThat(footer).contains("Space").contains("view");
+    }
+
+    @Test
+    void renderSnapshotDataShowsRoutesTable() {
+        TestMonitorContext snapshotCtx = new 
TestMonitorContext(dataWith(info), statusResponse())
+                .withSnapshot(snapshotResponse());
+        snapshotCtx.selectedPid = "1234";
+        JfrTab tab = new JfrTab(snapshotCtx, Runnable::run);
+
+        tab.onTabSelected();
+        tab.handleKeyEvent(KeyEvent.ofKey(KeyCode.F5, KeyModifiers.NONE));
+
+        await().atMost(5, TimeUnit.SECONDS).untilAsserted(() -> {
+            String rendered = TuiTestHelper.renderToString(tab, 140, 30);
+            assertThat(rendered).contains("Routes");
+        });
+    }
+
+    @Test
+    void renderSnapshotDataShowsProcessorsPanelForSelectedRoute() {
+        TestMonitorContext snapshotCtx = new 
TestMonitorContext(dataWith(info), statusResponse())
+                .withSnapshot(snapshotResponse());
+        snapshotCtx.selectedPid = "1234";
+        JfrTab tab = new JfrTab(snapshotCtx, Runnable::run);
+
+        tab.onTabSelected();
+
+        await().atMost(5, TimeUnit.SECONDS).untilAsserted(() -> {
+            String rendered = TuiTestHelper.renderToString(tab, 140, 30);
+            assertThat(rendered).contains("Enabled");
+        });
+
+        tab.handleKeyEvent(KeyEvent.ofKey(KeyCode.F5, KeyModifiers.NONE));
+
+        await().atMost(5, TimeUnit.SECONDS).untilAsserted(() -> {
+            String rendered = TuiTestHelper.renderToString(tab, 140, 30);
+            assertThat(rendered).contains("Processors [notify]");
+        });
+    }
+
+    @Test
+    void renderFooterShowsSortHintWhenSnapshotLoaded() {
+        TestMonitorContext snapshotCtx = new 
TestMonitorContext(dataWith(info), statusResponse())
+                .withSnapshot(snapshotResponse());
+        snapshotCtx.selectedPid = "1234";
+        JfrTab tab = new JfrTab(snapshotCtx, Runnable::run);
+
+        tab.onTabSelected();
+
+        await().atMost(5, TimeUnit.SECONDS).untilAsserted(() -> {
+            String rendered = TuiTestHelper.renderToString(tab, 140, 30);
+            assertThat(rendered).contains("Enabled");
+        });
+
+        tab.handleKeyEvent(KeyEvent.ofKey(KeyCode.F5, KeyModifiers.NONE));
+
+        await().atMost(5, TimeUnit.SECONDS).untilAsserted(() -> {
+            String rendered = TuiTestHelper.renderToString(tab, 140, 30);
+            assertThat(rendered).contains("order-in");
+
+            List<Span> footerSpans = new ArrayList<>();
+            tab.renderFooter(footerSpans);
+            String footer = footerSpans.stream().map(Span::content).reduce("", 
String::concat);
+            assertThat(footer).contains("s").contains("sort");
+        });
+    }
+
+    @Test
+    void viewSwitchingChangesActiveView() {
+        TestMonitorContext snapshotCtx = new 
TestMonitorContext(dataWith(info), statusResponse())
+                .withSnapshot(snapshotResponse());
+        snapshotCtx.selectedPid = "1234";
+        JfrTab tab = new JfrTab(snapshotCtx, Runnable::run);
+
+        tab.onTabSelected();
+        tab.handleKeyEvent(KeyEvent.ofKey(KeyCode.F5, KeyModifiers.NONE));
+        // Space cycles: Routes -> Processors -> Endpoints
+        tab.handleKeyEvent(KeyEvent.ofChar(' '));
+        tab.handleKeyEvent(KeyEvent.ofChar(' '));
+
+        await().atMost(5, TimeUnit.SECONDS).untilAsserted(() -> {
+            String rendered = TuiTestHelper.renderToString(tab, 140, 30);
+            assertThat(rendered).contains("Endpoints");
+        });
+    }
+
+    @Test
+    void promptToSnapshotWhenNoData() {
+        JfrTab tab = new JfrTab(ctx);
+        String rendered = TuiTestHelper.renderToString(tab, 140, 30);
+        assertThat(rendered).contains("F5");
+    }
+
     @Test
     void description() {
         JfrTab tab = new JfrTab(ctx);
@@ -111,9 +212,76 @@ class JfrTabRenderTest {
         return response;
     }
 
+    private static JsonObject snapshotResponse() {
+        JsonObject response = new JsonObject();
+        response.put("snapshot", true);
+        response.put("eventCount", 150);
+
+        JsonArray routes = new JsonArray();
+        JsonObject route1 = new JsonObject();
+        route1.put("routeId", "order-in");
+        route1.put("total", 100L);
+        route1.put("failed", 5L);
+        route1.put("minMs", 1.2);
+        route1.put("meanMs", 8.5);
+        route1.put("maxMs", 250.3);
+        routes.add(route1);
+        JsonObject route2 = new JsonObject();
+        route2.put("routeId", "notify");
+        route2.put("total", 50L);
+        route2.put("failed", 0L);
+        route2.put("minMs", 2.0);
+        route2.put("meanMs", 5.0);
+        route2.put("maxMs", 45.0);
+        routes.add(route2);
+        response.put("routes", routes);
+
+        JsonArray processors = new JsonArray();
+        JsonObject proc1 = new JsonObject();
+        proc1.put("processorId", "to1");
+        proc1.put("processorType", "to");
+        proc1.put("routeId", "order-in");
+        proc1.put("total", 100L);
+        proc1.put("failed", 3L);
+        proc1.put("minMs", 0.5);
+        proc1.put("meanMs", 5.2);
+        proc1.put("maxMs", 200.1);
+        processors.add(proc1);
+        response.put("processors", processors);
+
+        JsonArray endpoints = new JsonArray();
+        JsonObject ep1 = new JsonObject();
+        ep1.put("endpointUri", "kafka://orders");
+        ep1.put("total", 100L);
+        ep1.put("failed", 3L);
+        ep1.put("minMs", 0.8);
+        ep1.put("meanMs", 4.1);
+        ep1.put("maxMs", 180.5);
+        endpoints.add(ep1);
+        response.put("endpoints", endpoints);
+
+        response.put("failures", new JsonArray());
+        response.put("redeliveries", new JsonArray());
+
+        return response;
+    }
+
+    private static JsonObject statusResponse() {
+        JsonObject response = new JsonObject();
+        response.put("runtimeEvents", true);
+        JsonArray recordings = new JsonArray();
+        JsonObject rec = new JsonObject();
+        rec.put("name", "default");
+        rec.put("state", "Running");
+        recordings.add(rec);
+        response.put("recordings", recordings);
+        return response;
+    }
+
     private static final class TestMonitorContext extends MonitorContext {
 
         private final JsonObject response;
+        private JsonObject snapshotResp;
 
         private TestMonitorContext(
                                    AtomicReference<List<IntegrationInfo>> data,
@@ -122,8 +290,17 @@ class JfrTabRenderTest {
             this.response = response;
         }
 
+        TestMonitorContext withSnapshot(JsonObject snapshot) {
+            this.snapshotResp = snapshot;
+            return this;
+        }
+
         @Override
         JsonObject executeAction(String pid, JsonObject request, long 
timeoutMs) {
+            String command = request.getString("command");
+            if ("snapshot".equals(command) && snapshotResp != null) {
+                return snapshotResp;
+            }
             return response;
         }
     }

Reply via email to