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

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

commit 68dfc4d184c0796bb185b13ed36f948588ecbbe6
Author: Claus Ibsen <[email protected]>
AuthorDate: Fri Nov 6 23:29:41 2026 +0100

    CAMEL-26691: timeline - a second is wider when the screen allows, and the 
docs
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../ROOT/pages/camel-jbang-tui-observe.adoc        | 22 +++++++++++
 .../modules/ROOT/pages/camel-jbang-tui.adoc        |  2 +-
 .../dsl/jbang/core/commands/tui/TimelineTab.java   | 45 +++++++++++++++-------
 3 files changed, 55 insertions(+), 14 deletions(-)

diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-observe.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-observe.adoc
index cc4a7d617e96..b7d9d39f3456 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-observe.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-observe.adoc
@@ -240,6 +240,28 @@ right panel. While sending, the status shows a spinner.
 
 Beyond the core tabs described above, the TUI includes several specialized 
screens worth highlighting.
 
+=== Timeline
+
+The Timeline tab (under More > Observability) shows the last minute of the 
selected integration on one time axis, one
+column per second: what went wrong on top, and what it did to the routes under 
it. Read from left to right, it tells
+the story of an outage: the stock service answered 503 for fifteen seconds, 
the orders went to the dead letter channel,
+shipping had nothing to ship, and everything recovered by itself.
+
+image::jbang/camel-tui-timeline.png[The Timeline tab in an outage of the stock 
service: the errors per second on top, the routes under it, and the orders 
route in detail]
+
+* *Errors/s* stacks the errors per second by the kind of exception, with the 
HTTP status when there is one. With
+camel-chaos, the faults of each chaos rule are a layer of their own, marked 🎲, 
so an injected fault and a real one stay
+apart. When nothing went wrong the band is one line.
+* *Routes* has a line per route. The height is the exchanges done that second, 
and the colour the worst that happened to
+them: completed, recovered by redelivery, handled by the error handler (such 
as the dead letter channel), or failed. A
+second a chaos rule had the route down is shaded.
+* The selected route is shown in detail: its outcomes stacked per second, its 
mean processing time and its exchanges in
+flight. The screen opens on the route that had the worst time.
+
+*w* switches the window between 1, 2 and 5 minutes, *Space* pauses so a moment 
does not scroll away (*Left* and *Right*
+then go back and forth), and *Shift+F8* asks the AI to explain what happened, 
as a story of cause and effect. The
+timeline is recorded while the TUI watches the integration, so it opens with 
the last minutes, not an empty chart.
+
 === Chaos
 
 With xref:next@components:others:chaos.adoc[camel-chaos] (`camel run 
--chaos`), the Chaos tab (under More >
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 44efe2a3de04..b2e6ca9c4152 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
@@ -80,7 +80,7 @@ to any tab, or *Shift+F2* to go to a tab by name.
 The *More* menu (key *0*) opens a popup with tabs organized into groups:
 
 * *Routing* -- Browse Endpoints, Consumers, HTTP, Inflight, Producers, Route 
Controller
-* *Observability* -- Chaos, Circuit Breaker, Health, JFR, Metrics, Network 
Services, Exchange Events, Recovery Tasks, OpenTelemetry Spans
+* *Observability* -- Timeline, Chaos, Circuit Breaker, Health, JFR, Metrics, 
Network Services, Exchange Events, Recovery Tasks, OpenTelemetry Spans
 * *AI* -- Semantic (definitions, expert contracts, relationships, sample 
evaluation, Audit and Monitoring), Ollama (listed when an Ollama server is 
detected)
 * *Data* -- JDBC DataSource, Kafka, Secrets, SQL Query, SQL Trace
 * *JVM* -- Classpath, Heap Memory Histogram, Memory Usage, Memory Leak, 
Process, Startup, Threads
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TimelineTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TimelineTab.java
index ebe7474408d6..ec78fcb0fe1e 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TimelineTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TimelineTab.java
@@ -60,6 +60,9 @@ class TimelineTab extends AbstractTableTab {
     private static final int MAX_LABEL = 18;
 
     private final LongSupplier clock;
+    // the seconds a column has, and the characters a column is wide, as the 
last render worked them out
+    private int per = 1;
+    private int width = 1;
     private int window;
     // the second the screen stays at while paused, or -1 to follow now
     private long pausedAt = -1;
@@ -220,9 +223,12 @@ class TimelineTab extends AbstractTableTab {
         int inner = Math.max(20, area.width() - 2);
         int label = Math.min(MAX_LABEL,
                 Math.max(10, routes.stream().mapToInt(r -> 
r.route().length()).max().orElse(0))) + 3;
-        int columns = Math.max(10, Math.min(seconds(), inner - label - SUFFIX 
- 1));
-        int per = (int) Math.ceil((double) seconds() / columns);
-        columns = (seconds() + per - 1) / per;
+        // a second is 1 to 3 characters wide as the screen allows, or a 
character is several seconds
+        int room = Math.max(10, inner - label - SUFFIX - 1);
+        int per = room >= seconds() ? 1 : (int) Math.ceil((double) seconds() / 
room);
+        width = per == 1 ? Math.max(1, Math.min(3, room / seconds())) : 1;
+        this.per = per;
+        int columns = (seconds() + per - 1) / per * width;
 
         List<Line> lines = new ArrayList<>();
         renderCauses(lines, causes, anyCause, info, columns, per, label);
@@ -278,7 +284,7 @@ class TimelineTab extends AbstractTableTab {
                 continue;
             }
             Style style = causeStyle(colour++);
-            values.add(columnsOf(c.values(), per, false));
+            values.add(cols(c.values(), false));
             styles.add(style);
             legend.add(Span.styled("â–ˆ ", style));
             legend.add(Span.raw((c.rule() != null ? "🎲 " : "") + c.label() + " 
 "));
@@ -311,11 +317,11 @@ class TimelineTab extends AbstractTableTab {
         spans.add(selected
                 ? Span.styled(pad(">> " + name, label), Theme.selectionBg())
                 : Span.raw(pad("   " + name, label)));
-        long[] completed = columnsOf(r.completed(), per, false);
-        long[] recovered = columnsOf(r.recovered(), per, false);
-        long[] handled = columnsOf(r.handled(), per, false);
-        long[] failed = columnsOf(r.failed(), per, false);
-        long[] down = columnsOf(r.down(), per, true);
+        long[] completed = cols(r.completed(), false);
+        long[] recovered = cols(r.recovered(), false);
+        long[] handled = cols(r.handled(), false);
+        long[] failed = cols(r.failed(), false);
+        long[] down = cols(r.down(), true);
         long max = 1;
         for (int c = 0; c < columns; c++) {
             max = Math.max(max, total(completed[c], recovered[c], handled[c], 
failed[c]));
@@ -422,13 +428,13 @@ class TimelineTab extends AbstractTableTab {
                     .block(block(" Route ")).build(), area);
             return;
         }
-        List<long[]> values = List.of(columnsOf(r.completed(), per, false), 
columnsOf(r.recovered(), per, false),
-                columnsOf(r.handled(), per, false), columnsOf(r.failed(), per, 
false));
+        List<long[]> values = List.of(cols(r.completed(), false), 
cols(r.recovered(), false),
+                cols(r.handled(), false), cols(r.failed(), false));
         List<Style> styles = List.of(Theme.success(), Theme.warning(), 
handledStyle(), Theme.error());
         int rows = Math.max(2, Math.min(DETAIL_ROWS, area.height() - 5));
         lines.addAll(stacked(values, styles, rows, columns, label));
 
-        long[] mean = columnsOf(r.mean(), per, true);
+        long[] mean = cols(r.mean(), true);
         long maxMean = 0;
         long sumMean = 0;
         int n = 0;
@@ -450,7 +456,7 @@ class TimelineTab extends AbstractTableTab {
                 : "no exchange")));
         lines.add(Line.from(meanLine));
 
-        long[] inflight = columnsOf(r.inflight(), per, true);
+        long[] inflight = cols(r.inflight(), true);
         long maxInflight = 0;
         long last = 0;
         for (long v : r.inflight()) {
@@ -544,6 +550,19 @@ class TimelineTab extends AbstractTableTab {
         return sb.toString();
     }
 
+    /** The seconds as the columns of the chart: several seconds a column, or 
a second several characters wide. */
+    private long[] cols(long[] seconds, boolean highest) {
+        long[] columns = columnsOf(seconds, per, highest);
+        if (width == 1) {
+            return columns;
+        }
+        long[] wide = new long[columns.length * width];
+        for (int i = 0; i < wide.length; i++) {
+            wide[i] = columns[i / width];
+        }
+        return wide;
+    }
+
     /**
      * The seconds as columns of several seconds each: the sum of the counts, 
or the highest value of a level (a mean,
      * the in flight count). A column with no known second is -1.

Reply via email to