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 699f0798f7dc chore: camel-jbang - an integration that wrote no status 
for 15 seconds says so, instead of running at its last rate (#29524)
699f0798f7dc is described below

commit 699f0798f7dc3f207722ddec2576364b4e2d773d
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue Nov 10 07:46:44 2026 +0100

    chore: camel-jbang - an integration that wrote no status for 15 seconds 
says so, instead of running at its last rate (#29524)
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../ROOT/pages/camel-jbang-tui-observe.adoc        |   4 +
 .../dsl/jbang/core/commands/tui/AboutView.java     |  26 ++--
 .../core/commands/tui/DataRefreshService.java      |  33 +++--
 .../jbang/core/commands/tui/IntegrationInfo.java   |   7 +
 .../dsl/jbang/core/commands/tui/McpFacade.java     |   6 +-
 .../dsl/jbang/core/commands/tui/OverviewTab.java   |  17 +--
 .../dsl/jbang/core/commands/tui/StatusAge.java     | 141 ++++++++++++++++++++
 .../src/main/resources/tui/help/overview.md        |   8 +-
 .../dsl/jbang/core/commands/tui/AboutViewTest.java |  19 +++
 .../commands/tui/DataRefreshMembershipTest.java    |  20 ++-
 .../dsl/jbang/core/commands/tui/StatusAgeTest.java | 145 +++++++++++++++++++++
 11 files changed, 395 insertions(+), 31 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 ab7c0d28f6f1..98e4bc0533df 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
@@ -36,6 +36,10 @@ over the last 5 minutes, coloured as on the Timeline, with 
when the worst happen
 allows, and on a tall screen up to two rows high, with room around the panels. 
*+* and *-* zoom between 5, 2 and 1
 minutes (zooming in centres on the last trouble, so you see what the other 
routes did then), and *Left* and *Right*
 move a zoomed window. *4* opens the Timeline for each second.
+* *Watch* has the errors to look at. An integration that wrote no status for 
15 seconds, while its process runs, says
+since when instead: its numbers are of then. Its CPU tells why: busy when its 
heap is full and the garbage collector runs
+all the time, idle when a debugger holds it. A stop may not reach it, and 
*F10* kills it. The Overview table shows it
+as `No status`.
 
 `camel get about` shows the same from the command line (see 
xref:camel-jbang-diagnostics.adoc#_about[Diagnostics]).
 
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutView.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutView.java
index 9d83e720e76d..630723f681ac 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutView.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutView.java
@@ -596,8 +596,9 @@ final class AboutView {
         String p = purpose;
         String pFrom = purposeFrom;
         List<Line> head = new ArrayList<>();
-        head.add(Line.from(Span.raw(" "), Span.raw(meta(info, o)),
-                Span.styled(info.vanishing ? "  ○ stopped" : "  ● running",
+        head.add(Line.from(Span.raw(" "), Span.raw(meta(info, o)), 
StatusAge.stale(info)
+                ? Span.styled("  ✗ no status", Theme.error())
+                : Span.styled(info.vanishing ? "  ○ stopped" : "  ● running",
                         info.vanishing ? Theme.muted() : Theme.success())));
         if (p != null) {
             for (String l : wrap(p, Math.max(20, width - 8), 2)) {
@@ -959,7 +960,7 @@ final class AboutView {
             // the exchanges of the route it starts over the last minutes, and 
the rate now
             long[] perSecond = e.route() != null ? routePerSecond(info, 
e.route()) : null;
             if (perSecond != null) {
-                in.add(sparkLine("   ", perSecond, spark));
+                in.add(sparkLine("   ", perSecond, spark, 
StatusAge.stale(info)));
                 inTargets.add(null);
             }
         }
@@ -1004,7 +1005,7 @@ final class AboutView {
             if (perSecond != null) {
                 // the sparkline in what "    from <route>  " and the rate 
leave of the column
                 int room = outWidth - 9 - s.route().length() - 2 - 8;
-                from.addAll(sparkLine("  ", perSecond, Math.max(4, 
Math.min(spark, room)))
+                from.addAll(sparkLine("  ", perSecond, Math.max(4, 
Math.min(spark, room)), StatusAge.stale(info))
                         .spans());
             }
             out.add(Line.from(from));
@@ -1036,13 +1037,16 @@ final class AboutView {
         return null;
     }
 
-    /** The values as a sparkline and the rate now, or only idle when there 
was nothing. */
-    private static Line sparkLine(String indent, long[] values, int width) {
+    /**
+     * The values as a sparkline and the rate now, or only idle when there was 
nothing; the rate is not known when the
+     * integration wrote no status of late.
+     */
+    private static Line sparkLine(String indent, long[] values, int width, 
boolean noStatus) {
         if (Arrays.stream(values).allMatch(v -> v <= 0)) {
-            return Line.from(Span.raw(indent), Span.styled("idle", 
Theme.muted()));
+            return Line.from(Span.raw(indent), Span.styled(noStatus ? "-" : 
"idle", Theme.muted()));
         }
         return Line.from(Span.raw(indent), Span.styled(spark(values, width), 
Theme.info()),
-                Span.styled("  " + rate(values), Theme.muted()));
+                Span.styled("  " + (noStatus ? "-" : rate(values)), 
Theme.muted()));
     }
 
     /** The sparklines of In fill its column, between {@value #SPARK_WIDTH} 
and {@value #MAX_SPARK_WIDTH} wide. */
@@ -1118,7 +1122,11 @@ final class AboutView {
         lines.add(happened(info, width));
         List<Span> watch = new ArrayList<>();
         watch.add(Span.styled(" Watch    ", Theme.muted()));
-        if (info.errorCount > 0) {
+        List<Span> noStatus = StatusAge.watch(info);
+        if (!noStatus.isEmpty()) {
+            // its numbers are of when it stopped writing its status
+            watch.addAll(noStatus);
+        } else if (info.errorCount > 0) {
             // errors that stopped a minute ago are history: the line says so, 
and calms down. A minute, not the 5 of
             // Last 5m and the strips (which still show them): a fix that 
reloads should not stay red for long
             long lastError = lastError(info);
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DataRefreshService.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DataRefreshService.java
index a1a8f13fe86b..8c2910d0c956 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DataRefreshService.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DataRefreshService.java
@@ -323,9 +323,12 @@ class DataRefreshService {
                     continue;
                 }
                 IntegrationInfo info = StatusParser.parseIntegration(ph, root);
+                if (info != null) {
+                    info.statusWrittenAt = writtenAt(pid);
+                }
                 if (info != null && ctx != null) {
                     ctx.chaosMarks.record(info);
-                    ctx.timeline.record(info, writtenAt(pid));
+                    ctx.timeline.record(info, info.statusWrittenAt);
                     mergeTimelineFile(pid);
                     timelineLog.read(info.pid, logFile(info), ctx.timeline);
                 }
@@ -334,16 +337,10 @@ class DataRefreshService {
                     if (prev != null) {
                         info.errors = prev.errors;
                     }
+                    StatusAge.observeCpu(info, prev, ph, now);
                     rates.fill(info, now);
                     infos.add(info);
-                    metrics.updateThroughputHistory(info);
-                    metrics.updateEndpointHistory(info);
-                    metrics.updateServiceHistory(info);
-                    metrics.updateCbHistory(info);
-                    metrics.updateHeapHistory(info);
-                    if (ctx != null) {
-                        ctx.healthHistory.record(info, 
System.currentTimeMillis() / 1000);
-                    }
+                    recordHistories(info);
                     metrics.updateLoadMetrics(ph, info);
                     detectReload(info);
                 }
@@ -911,6 +908,24 @@ class DataRefreshService {
         return msg;
     }
 
+    /**
+     * The charts and strips of the integration go on by a second: from a 
status of late only, as one of then would draw
+     * its last rate on and on.
+     */
+    void recordHistories(IntegrationInfo info) {
+        if (StatusAge.stale(info)) {
+            return;
+        }
+        metrics.updateThroughputHistory(info);
+        metrics.updateEndpointHistory(info);
+        metrics.updateServiceHistory(info);
+        metrics.updateCbHistory(info);
+        metrics.updateHeapHistory(info);
+        if (ctx != null) {
+            ctx.healthHistory.record(info, System.currentTimeMillis() / 1000);
+        }
+    }
+
     private List<Long> findPids(String name) {
         return TuiHelper.findPids(name, statusFileResolver);
     }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/IntegrationInfo.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/IntegrationInfo.java
index bc5304001ff8..a559de4b30ce 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/IntegrationInfo.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/IntegrationInfo.java
@@ -98,6 +98,13 @@ class IntegrationInfo {
     int threadCount;
     int peakThreadCount;
     boolean vanishing;
+    // when it last wrote its status file (epoch millis), 0 when not known; 
StatusAge tells one that stopped writing it
+    long statusWrittenAt;
+    // while it writes no status: the CPU time of its process, when it was 
read (a MonotonicClock time), and the CPU
+    // since the read before in percent of one core (-1 not known yet)
+    long cpuMillis = -1;
+    long cpuReadAt;
+    double cpuPercent = -1;
     // when it was found gone, a MonotonicClock time: it fades out from then
     long vanishStart;
     final List<RouteInfo> routes = new ArrayList<>();
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
index 2486b66e6d78..76c4295defea 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
@@ -1726,7 +1726,8 @@ class McpFacade {
         }
         if ("stop-all".equals(action)) {
             bridge.stopAll();
-            return "Stopping all processes";
+            // the user answers the dialog: the processes stop when they 
confirm it
+            return "Asked the user to stop all processes: they confirm it in 
the Stop All dialog";
         }
         if (nameOrPid != null && !nameOrPid.isBlank()) {
             String pid = findPidByNameOrPid(nameOrPid.trim());
@@ -1801,7 +1802,8 @@ class McpFacade {
             }
             case "stop" -> {
                 bridge.stopProcess(false);
-                yield "Stopping " + name;
+                String stuck = 
StatusAge.stopAnswer(ctx.findSelectedIntegration());
+                yield stuck != null ? stuck : "Stopping " + name;
             }
             case "kill" -> {
                 bridge.stopProcess(true);
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTab.java
index aaad494296c4..01f9c21a214d 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTab.java
@@ -526,7 +526,7 @@ class OverviewTab extends AbstractTab {
                 }
                 Line nameLine = Line.from(nameSpans);
                 Style failStyle = info.failed > 0 ? Theme.error().bold() : 
Style.EMPTY;
-                String throughputDisplay = info.throughput;
+                String throughputDisplay = StatusAge.stale(info) ? "-" : 
info.throughput;
                 if (throughputDisplay == null || 
"0.00".equals(throughputDisplay)) {
                     LinkedList<Long> tpHist = throughputHistory.get(info.pid);
                     if (tpHist != null && !tpHist.isEmpty()) {
@@ -678,13 +678,12 @@ class OverviewTab extends AbstractTab {
                 } else if ("Running".equals(stateText) && info.routeStarted == 
0 && info.routeTotal > 0) {
                     stateText = "Stopped";
                 }
-                if (info.reloadError != null && !"Stopping".equals(stateText)) 
{
-                    // its last reload failed, as camel ps shows it; the info 
panel says why
-                    stateText = "Error";
-                }
+                // its last reload failed, as camel ps shows it (the info 
panel says why); or it wrote no status of late
+                stateText = StatusAge.state(info,
+                        info.reloadError != null && 
!"Stopping".equals(stateText) ? "Error" : stateText);
                 Style statusStyle = switch (stateText) {
                     case "Started", "Running" -> Theme.success();
-                    case "Stopped", "Error" -> Theme.error();
+                    case "Stopped", "Error", StatusAge.NO_STATUS -> 
Theme.error();
                     default -> Theme.warning();
                 };
 
@@ -709,7 +708,7 @@ class OverviewTab extends AbstractTab {
                     nameSpans.add(Span.styled(" " + TuiIcons.README, 
Style.EMPTY));
                 }
                 Line nameLine = TuiHelper.fitLine(nameSpans, nameColumn);
-                String throughputDisplay = info.throughput;
+                String throughputDisplay = StatusAge.stale(info) ? "-" : 
info.throughput;
                 if (throughputDisplay == null || 
"0.00".equals(throughputDisplay)) {
                     LinkedList<Long> tpHist = throughputHistory.get(info.pid);
                     if (tpHist != null && !tpHist.isEmpty()) {
@@ -1633,6 +1632,7 @@ class OverviewTab extends AbstractTab {
             if (info.reloadError != null) {
                 row.put("reloadError", info.reloadError);
             }
+            StatusAge.describe(info, row);
             row.put("ready", info.ready);
             // info.uptime holds the process start time (epoch millis), which 
the screen shows as an elapsed
             // duration; export the same duration plus the raw values with 
unambiguous names so an AI reading
@@ -1645,7 +1645,8 @@ class OverviewTab extends AbstractTab {
             row.put("exchangesTotal", info.exchangesTotal);
             row.put("failed", info.failed);
             row.put("inflight", info.inflight);
-            row.put("throughput", info.throughput);
+            // its rate now is not known without a status of late
+            row.put("throughput", StatusAge.stale(info) ? null : 
info.throughput);
             row.put("routeStarted", info.routeStarted);
             row.put("routeTotal", info.routeTotal);
             row.put("meanTime", info.meanTime);
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusAge.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusAge.java
new file mode 100644
index 000000000000..df9888bbc817
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusAge.java
@@ -0,0 +1,141 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.time.LocalTime;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+import dev.tamboui.text.Span;
+import org.apache.camel.util.TimeUtils;
+import org.apache.camel.util.json.JsonObject;
+
+/**
+ * An integration that wrote no status of late. It writes its status file 
every second; when it has not for a while, and
+ * its process is still there, the numbers the TUI has are those of that 
moment: the JVM is stuck (a heap that is full,
+ * where the garbage collector runs all the time; a debugger that suspended 
it), or its CLI connector stopped
+ * (CAMEL-26794). The screens then say so instead of showing it running at its 
last rate.
+ */
+final class StatusAge {
+
+    /** The state of an integration without a status of late, in the STATUS 
column. */
+    static final String NO_STATUS = "No status";
+
+    // the status is written every second (100 ms while debugging), and a 
pause of the garbage collector takes seconds
+    static final long AFTER_MILLIS = 15_000;
+    // the CPU of a JVM that is busy without a status: the garbage collector, 
or a loop
+    private static final double BUSY_CPU_PERCENT = 50;
+    private static final DateTimeFormatter CLOCK = 
DateTimeFormatter.ofPattern("HH:mm:ss", Locale.US);
+
+    private StatusAge() {
+    }
+
+    /** Whether the integration wrote no status for a while, at the time now 
(epoch millis). */
+    static boolean stale(IntegrationInfo info, long now) {
+        return !info.phantom && !info.vanishing && info.statusWrittenAt > 0 && 
now - info.statusWrittenAt > AFTER_MILLIS;
+    }
+
+    static boolean stale(IntegrationInfo info) {
+        return stale(info, System.currentTimeMillis());
+    }
+
+    /** The state to show: {@link #NO_STATUS} when it wrote no status of late, 
else the state it has. */
+    static String state(IntegrationInfo info, String state) {
+        return stale(info) ? NO_STATUS : state;
+    }
+
+    /**
+     * The CPU the process used since the last read, in percent of one core, 
while it writes no status: the JVM that is
+     * busy (the garbage collector) is told from the one that is held (a 
debugger, a deadlock).
+     *
+     * @param now a MonotonicClock time
+     */
+    static void observeCpu(IntegrationInfo info, IntegrationInfo previous, 
ProcessHandle process, long now) {
+        if (!stale(info)) {
+            return;
+        }
+        info.cpuMillis = 
process.info().totalCpuDuration().map(Duration::toMillis).orElse(-1L);
+        info.cpuReadAt = now;
+        if (previous == null) {
+            return;
+        }
+        if (info.cpuMillis >= 0 && previous.cpuMillis >= 0 && now > 
previous.cpuReadAt) {
+            info.cpuPercent = 100.0 * (info.cpuMillis - previous.cpuMillis) / 
(now - previous.cpuReadAt);
+        } else {
+            info.cpuPercent = previous.cpuPercent;
+        }
+    }
+
+    /** The Watch line of About for an integration that wrote no status of 
late, else none. */
+    static List<Span> watch(IntegrationInfo info) {
+        List<Span> spans = new ArrayList<>();
+        if (!stale(info)) {
+            return spans;
+        }
+        spans.add(Span.styled("✗ no status since " + 
clock(info.statusWrittenAt), Theme.error()));
+        spans.add(Span.styled(" (" + 
TimeUtils.printSince(info.statusWrittenAt) + " ago)", Theme.muted()));
+        String why = why(info.cpuPercent);
+        if (why != null) {
+            spans.add(Span.styled(": " + why, Theme.error()));
+        }
+        spans.add(Span.styled(" · its numbers are of then, and a stop may not 
reach it", Theme.muted()));
+        spans.add(Span.raw("   "));
+        spans.add(Span.styled(" F10 ", Theme.hintKey()));
+        spans.add(Span.raw(" stop or kill"));
+        return spans;
+    }
+
+    /** What the CPU of a JVM without a status says, or null when it is not 
known yet. */
+    static String why(double cpuPercent) {
+        if (cpuPercent < 0) {
+            return null;
+        }
+        String cpu = String.format(Locale.US, "%.0f%% CPU", cpuPercent);
+        return cpuPercent >= BUSY_CPU_PERCENT
+                ? "busy at " + cpu + ", as when its heap is full and the 
garbage collector runs all the time"
+                : "idle at " + cpu + ", as when a debugger holds it";
+    }
+
+    /** For the row of the integration that a tool reads: since when it wrote 
no status, and its CPU. */
+    static void describe(IntegrationInfo info, JsonObject row) {
+        if (!stale(info)) {
+            return;
+        }
+        row.put("noStatusSince", 
Instant.ofEpochMilli(info.statusWrittenAt).toString());
+        if (info.cpuPercent >= 0) {
+            row.put("cpuPercent", Math.round(info.cpuPercent));
+        }
+    }
+
+    /** The answer of a stop of an integration that wrote no status of late, 
else null. */
+    static String stopAnswer(IntegrationInfo info) {
+        if (info == null || !stale(info)) {
+            return null;
+        }
+        return "Stopping " + info.name + ", but it wrote no status for " + 
TimeUtils.printSince(info.statusWrittenAt)
+               + ", so it may not read the stop either: kill stops it";
+    }
+
+    private static String clock(long epochMillis) {
+        return LocalTime.ofInstant(Instant.ofEpochMilli(epochMillis), 
ZoneId.systemDefault()).format(CLOCK);
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/overview.md
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/overview.md
index a7e9943d64dd..5ed841d8d90c 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/overview.md
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/overview.md
@@ -35,7 +35,11 @@ happened to it, with no AI:
   when the last one did, routes not started, exchanges in flight. Errors
   that stopped more than a minute ago are history: the line says there
   were none in the last minute, with the earlier ones dimmed (Last 5m and
-  What happened still show them).
+  What happened still show them). An integration that wrote no status
+  for 15 seconds says since when instead, and what its CPU says: busy
+  when its heap is full and the garbage collector runs all the time,
+  idle when a debugger holds it. Its numbers are of then, and a stop
+  may not reach it; `F10` kills it.
 - **Next** — the keys that lead deeper: **4** the Timeline, **5** the
   Diagram, **2** the source.
 - **What happened** — a strip per route over the last 5 minutes (as wide
@@ -70,7 +74,7 @@ Each row represents one running Camel integration:
 - **PID** — Process ID of the JVM running this integration
 - **NAME** — Name of the integration (from the route file or application 
configuration). Example: `camel-demo`, `my-app`
 - **VERSION** — Camel version the integration is running on (e.g., `4.21.0`)
-- **STATUS** — Current lifecycle state: `Running` (processing messages), 
`Started` (ready), or `Stopped`
+- **STATUS** — Current lifecycle state: `Running` (processing messages), 
`Started` (ready), or `Stopped`; `No status` when it wrote no status for 15 
seconds while its process runs (its MSG/S is then `-`)
 - **AGE** — How long the integration has been running (e.g., `2m30s`, `1h15m`)
 - **MSG/S** or **MSG/M** — Messages processed per second or per minute 
(current throughput). The rate unit can be configured in settings. This is the 
overall rate across all routes
 - **TOTAL** — Total number of exchanges (messages) processed since the 
integration started
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutViewTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutViewTest.java
index 245204ed0ee2..0bdb12d6c214 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutViewTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutViewTest.java
@@ -156,6 +156,25 @@ class AboutViewTest {
         assertThat(render(new AboutView(ctx), info, 200, 40)).contains("last 
5m, a column is 2s");
     }
 
+    @Test
+    void anIntegrationWithoutAStatusOfLateSaysSoInsteadOfRunning() {
+        IntegrationInfo info = new IntegrationInfo();
+        info.pid = "1234";
+        info.errorCount = 2;
+        MonitorContext ctx = new MonitorContext(new 
AtomicReference<>(List.of(info)), new AtomicReference<>(List.of()));
+        info.statusWrittenAt = System.currentTimeMillis();
+        assertThat(render(new AboutView(ctx), info)).contains("● 
running").contains("✗ 2 errors");
+
+        // a timer-log held with kill -STOP: its last status 30 seconds ago, 
and no CPU since
+        info.statusWrittenAt = System.currentTimeMillis() - 30_000;
+        info.cpuPercent = 0;
+        String rendered = render(new AboutView(ctx), info);
+        assertThat(rendered).contains("✗ no status").doesNotContain("● 
running")
+                .contains("(30s ago): idle at 0% CPU, as when a debugger holds 
it")
+                // what is to watch now; its errors are of then
+                .doesNotContain("2 errors");
+    }
+
     @Test
     void watchCalmsDownWhenTheErrorsStopped() {
         IntegrationInfo info = new IntegrationInfo();
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/DataRefreshMembershipTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/DataRefreshMembershipTest.java
index 7f59653f8e34..ac61da9bcc27 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/DataRefreshMembershipTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/DataRefreshMembershipTest.java
@@ -31,7 +31,8 @@ import static org.assertj.core.api.Assertions.assertThat;
 
 /**
  * A new or stopped integration is noticed on every tab: the cheap look at the 
status files in ~/.camel asks for a full
- * scan when they change, at most once a second. Lookups that wait for the 
diagram retry until found.
+ * scan when they change, at most once a second. Lookups that wait for the 
diagram retry until found. A status that is
+ * not of late adds nothing to the charts.
  */
 @Isolated
 class DataRefreshMembershipTest {
@@ -109,6 +110,23 @@ class DataRefreshMembershipTest {
         assertThat(calls.get()).isEqualTo(1);
     }
 
+    @Test
+    void aStatusOfThenAddsNothingToTheCharts() {
+        DataRefreshService service = newService();
+        IntegrationInfo info = new IntegrationInfo();
+        info.pid = "562838";
+        info.throughput = "1.00";
+        info.statusWrittenAt = System.currentTimeMillis();
+        service.recordHistories(info);
+        
assertThat(service.metrics().getThroughputHistory().get("562838")).hasSize(1);
+
+        // held with kill -STOP a minute ago: its status still says 1 msg/s, 
and the chart does not take it on
+        info.statusWrittenAt = System.currentTimeMillis() - 60_000;
+        service.metrics().getThroughputHistory().clear();
+        service.recordHistories(info);
+        
assertThat(service.metrics().getThroughputHistory()).doesNotContainKey("562838");
+    }
+
     @Test
     void anExampleIsSelectedByTheNameItGivesItself() {
         assertThat(DataRefreshService.isLaunched("fail-well/circuit-breaker", 
"circuit-breaker")).isTrue();
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusAgeTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusAgeTest.java
new file mode 100644
index 000000000000..db6145dcca8a
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusAgeTest.java
@@ -0,0 +1,145 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.time.Instant;
+import java.util.HashSet;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+import dev.tamboui.text.Span;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * The memory-leak example after a few hours: its heap is full, the garbage 
collector runs all the time, and it wrote
+ * its status file the last time at 18:36. The TUI showed it running at 8.52 
msg/s since; now it says it has no status.
+ */
+class StatusAgeTest {
+
+    private IntegrationInfo info;
+    private MonitorContext ctx;
+
+    @BeforeEach
+    void setUp() {
+        Theme.resetForTesting();
+        info = new IntegrationInfo();
+        info.pid = "500874";
+        info.name = "memory-leak";
+        info.state = 5;
+        info.throughput = "8.52";
+        info.statusWrittenAt = System.currentTimeMillis();
+        ctx = new MonitorContext(new AtomicReference<>(List.of(info)), new 
AtomicReference<>(List.of()));
+        ctx.selectedPid = info.pid;
+    }
+
+    @AfterEach
+    void tearDown() {
+        Theme.resetForTesting();
+    }
+
+    @Test
+    void aStatusOfAFewSecondsAgoIsNoReason() {
+        long now = System.currentTimeMillis();
+        info.statusWrittenAt = now - 5_000;
+        assertThat(StatusAge.stale(info, now)).isFalse();
+        info.statusWrittenAt = now - StatusAge.AFTER_MILLIS - 1;
+        assertThat(StatusAge.stale(info, now)).isTrue();
+        // not known, or an opened project that does not run
+        info.statusWrittenAt = 0;
+        assertThat(StatusAge.stale(info, now)).isFalse();
+        info.statusWrittenAt = now - 60_000;
+        info.phantom = true;
+        assertThat(StatusAge.stale(info, now)).isFalse();
+    }
+
+    @Test
+    void theOverviewSaysItHasNoStatusInsteadOfItsLastRate() {
+        stuckFor(4 * 60_000);
+
+        OverviewTab tab = new OverviewTab(ctx, new MetricsCollector(), new 
HashSet<>(), () -> {
+        }).showIntegrations();
+        String row = TuiTestHelper.renderToString(tab, 160, 30).lines()
+                .filter(l -> 
l.contains("memory-leak")).findFirst().orElseThrow();
+        
assertThat(row).contains(StatusAge.NO_STATUS).doesNotContain("Running").doesNotContain("8.52");
+
+        JsonObject json = (JsonObject) ((JsonArray) 
tab.getTableDataAsJson().get("rows")).get(0);
+        assertThat(json).containsEntry("noStatusSince", 
Instant.ofEpochMilli(info.statusWrittenAt).toString())
+                .containsEntry("cpuPercent", 642L).containsEntry("throughput", 
null);
+    }
+
+    @Test
+    void theWatchLineSaysSinceWhenAndWhatItsCpuSays() {
+        stuckFor(4 * 60_000);
+
+        String watch = text(StatusAge.watch(info));
+        assertThat(watch).startsWith("✗ no status since ").contains("(4m")
+                .contains("busy at 642% CPU, as when its heap is full")
+                .contains("F10  stop or kill");
+
+        info.cpuPercent = 0.4;
+        assertThat(text(StatusAge.watch(info))).contains("idle at 0% CPU, as 
when a debugger holds it");
+        // its CPU is not known before the second read
+        info.cpuPercent = -1;
+        assertThat(text(StatusAge.watch(info))).doesNotContain("CPU");
+        // and a running integration has no such line
+        info.statusWrittenAt = System.currentTimeMillis();
+        assertThat(StatusAge.watch(info)).isEmpty();
+    }
+
+    @Test
+    void aStopSaysItMayNotReachIt() {
+        assertThat(StatusAge.stopAnswer(info)).isNull();
+        stuckFor(4 * 60_000);
+        assertThat(StatusAge.stopAnswer(info))
+                .isEqualTo("Stopping memory-leak, but it wrote no status for 
4m0s, so it may not read the stop either:"
+                           + " kill stops it");
+    }
+
+    @Test
+    void theCpuIsMeasuredBetweenTwoReadsWhileItHasNoStatus() {
+        ProcessHandle self = ProcessHandle.current();
+        StatusAge.observeCpu(info, null, self, 1_000);
+        // it wrote its status: nothing to measure
+        assertThat(info.cpuMillis).isEqualTo(-1);
+
+        info.statusWrittenAt = System.currentTimeMillis() - 60_000;
+        IntegrationInfo previous = new IntegrationInfo();
+        previous.cpuMillis = 0;
+        previous.cpuReadAt = 1_000;
+        StatusAge.observeCpu(info, previous, self, 2_000);
+        assertThat(info.cpuReadAt).isEqualTo(2_000);
+        // the CPU of this JVM since it started, over one second
+        assertThat(info.cpuPercent).isGreaterThan(0);
+    }
+
+    private void stuckFor(long millis) {
+        info.statusWrittenAt = System.currentTimeMillis() - millis;
+        info.cpuPercent = 642;
+    }
+
+    private static String text(List<Span> spans) {
+        StringBuilder sb = new StringBuilder();
+        spans.forEach(s -> sb.append(s.content()));
+        return sb.toString();
+    }
+}

Reply via email to