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

davsclaus pushed a commit to branch chore-tui-health-page
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 547f38ac3f67f4379bcd9bdd584677f5aafecab7
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Nov 9 17:36:50 2026 +0100

    chore: camel-jbang - Health is a page: all good in one look, or what is 
wrong first, with ready and live second by second
    
    A table of the checks Camel runs on its own, all saying UP, told little. The
    tab opens on a page:
    
    - all UP: a thumbs up, ready and live, since when it is up, how often the
      checks ran and whether they ever failed, the checks Camel runs on its own
      in one line, and your own checks by name
    - not all UP: NOT READY or NOT LIVE in the title, the checks that are not UP
      first (since when, the message, how often they failed, when they were last
      UP), what it means (taken out of the service, or restarted), and the other
      checks in one line
    - under both, ready and live second by second over the last minutes, as the
      TUI read them, so a check that went DOWN and came back is seen
    
    t shows the checks one by one in the table as before, d there only the DOWN
    ones. The More menu has a DOWN badge on Health. The details of the checks
    (since, last up, invocations, failures) are parsed from the status, and
    tui_get_table gives ready, live and them.
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../core/commands/tui/DataRefreshService.java      |   6 +
 .../jbang/core/commands/tui/HealthCheckInfo.java   |   9 +
 .../dsl/jbang/core/commands/tui/HealthHistory.java |  91 +++++++
 .../dsl/jbang/core/commands/tui/HealthPage.java    | 284 +++++++++++++++++++++
 .../dsl/jbang/core/commands/tui/HealthTab.java     |  42 ++-
 .../jbang/core/commands/tui/IntegrationInfo.java   |   3 +
 .../jbang/core/commands/tui/MonitorContext.java    |   2 +
 .../dsl/jbang/core/commands/tui/PopupManager.java  |   7 +
 .../dsl/jbang/core/commands/tui/StatusParser.java  |  41 ++-
 .../src/main/resources/tui/help/health.md          |  37 ++-
 .../core/commands/tui/HealthTabRenderTest.java     | 109 +++++++-
 .../jbang/core/commands/tui/StatusParserTest.java  |  20 ++
 12 files changed, 629 insertions(+), 22 deletions(-)

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 69313a523c98..a1a8f13fe86b 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
@@ -341,6 +341,9 @@ class DataRefreshService {
                     metrics.updateServiceHistory(info);
                     metrics.updateCbHistory(info);
                     metrics.updateHeapHistory(info);
+                    if (ctx != null) {
+                        ctx.healthHistory.record(info, 
System.currentTimeMillis() / 1000);
+                    }
                     metrics.updateLoadMetrics(ph, info);
                     detectReload(info);
                 }
@@ -400,6 +403,9 @@ class DataRefreshService {
                 boolean wasExplicitStop = stoppingPids.remove(prev.pid);
                 if (wasExplicitStop) {
                     metrics.removeVanished(prev.pid);
+                    if (ctx != null) {
+                        ctx.healthHistory.remove(prev.pid);
+                    }
                 } else {
                     vanishing.put(prev.pid, new VanishingInfo(prev, 
MonotonicClock.millis()));
                 }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HealthCheckInfo.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HealthCheckInfo.java
index d4988d9073f3..6545b86ae9eb 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HealthCheckInfo.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HealthCheckInfo.java
@@ -23,4 +23,13 @@ class HealthCheckInfo {
     boolean readiness;
     boolean liveness;
     String message;
+    /** When the check went to its state (epoch millis): UP since, DOWN since; 
0 when not known. */
+    long since;
+    /** When the check was last UP (epoch millis), 0 when not known. */
+    long lastUp;
+    /** How often the check ran, and how often it failed. */
+    long invocations;
+    long failures;
+    /** When it last failed (epoch millis), 0 when it did not. */
+    long lastFailure;
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HealthHistory.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HealthHistory.java
new file mode 100644
index 000000000000..3b8d15b1e788
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HealthHistory.java
@@ -0,0 +1,91 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.util.Arrays;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Whether each integration was ready and live, second by second over the last 
minutes, as the TUI read its health: a
+ * check that went DOWN and came back between two looks at the Health tab is 
seen on its strip.
+ */
+final class HealthHistory {
+
+    static final int SECONDS = 300;
+    /** Not known for that second: before the TUI read the integration, or the 
integration did not say. */
+    static final byte UNKNOWN = -1;
+    static final byte READY = 1;
+    static final byte LIVE = 2;
+
+    // per pid: a ring of the last SECONDS seconds, and the second its last 
value is of
+    private final Map<String, Ring> rings = new ConcurrentHashMap<>();
+
+    private static final class Ring {
+        final byte[] values = new byte[SECONDS];
+        long last = -1;
+
+        Ring() {
+            Arrays.fill(values, UNKNOWN);
+        }
+    }
+
+    /** Records the state of the integration at the second (epoch seconds) it 
was read. */
+    void record(IntegrationInfo info, long second) {
+        if (info.pid == null || info.healthReady == null && info.healthLive == 
null) {
+            return;
+        }
+        byte value = (byte) ((Boolean.TRUE.equals(info.healthReady) ? READY : 
0)
+                             | (Boolean.TRUE.equals(info.healthLive) ? LIVE : 
0));
+        Ring ring = rings.computeIfAbsent(info.pid, k -> new Ring());
+        synchronized (ring) {
+            if (ring.last >= 0 && second > ring.last) {
+                // the seconds in between were not read: not known
+                for (long s = ring.last + 1; s < second && s <= ring.last + 
SECONDS; s++) {
+                    ring.values[(int) (s % SECONDS)] = UNKNOWN;
+                }
+            }
+            if (second >= ring.last) {
+                ring.values[(int) (second % SECONDS)] = value;
+                ring.last = second;
+            }
+        }
+    }
+
+    /** The values of the seconds that end with the second given, oldest 
first, UNKNOWN where not known. */
+    byte[] strip(String pid, long end, int seconds) {
+        byte[] out = new byte[seconds];
+        Arrays.fill(out, UNKNOWN);
+        Ring ring = pid != null ? rings.get(pid) : null;
+        if (ring == null) {
+            return out;
+        }
+        synchronized (ring) {
+            for (int i = 0; i < seconds; i++) {
+                long s = end - seconds + 1 + i;
+                if (s <= ring.last && s > ring.last - SECONDS) {
+                    out[i] = ring.values[(int) (s % SECONDS)];
+                }
+            }
+        }
+        return out;
+    }
+
+    void remove(String pid) {
+        rings.remove(pid);
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HealthPage.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HealthPage.java
new file mode 100644
index 000000000000..573acb7ce17e
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HealthPage.java
@@ -0,0 +1,284 @@
+/*
+ * 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.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.List;
+
+import dev.tamboui.layout.Rect;
+import dev.tamboui.style.Style;
+import dev.tamboui.terminal.Frame;
+import dev.tamboui.text.CharWidth;
+import dev.tamboui.text.Line;
+import dev.tamboui.text.Span;
+import dev.tamboui.text.Text;
+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 org.apache.camel.util.TimeUtils;
+
+/**
+ * The Health tab as a page: when every check is UP, that it is all good in 
one look, how long, and which checks Camel
+ * runs on its own; when one is not, the checks that are not first, what that 
means for the integration, and the other
+ * checks folded into one line. Under both, whether it was ready and live 
second by second, so a check that went DOWN
+ * and came back is seen.
+ */
+final class HealthPage {
+
+    private static final DateTimeFormatter TIME = 
DateTimeFormatter.ofPattern("HH:mm:ss").withZone(ZoneId.systemDefault());
+
+    private HealthPage() {
+    }
+
+    static void render(Frame frame, Rect area, IntegrationInfo info, 
HealthHistory history, long now) {
+        List<HealthCheckInfo> checks = info.healthChecks;
+        List<HealthCheckInfo> notUp = checks.stream().filter(hc -> 
!"UP".equals(hc.state)).toList();
+        boolean notReady = Boolean.FALSE.equals(info.healthReady);
+        boolean notLive = Boolean.FALSE.equals(info.healthLive);
+        String title = notLive ? " Health โ”€ โœ— NOT LIVE " : notReady ? " Health 
โ”€ โœ— NOT READY " : " Health ";
+        Block block = 
Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
+                .borderStyle(notUp.isEmpty() ? Style.EMPTY : 
Theme.error()).title(title).build();
+        int width = Math.max(0, area.width() - 2);
+
+        List<Line> lines = new ArrayList<>();
+        lines.add(Line.from(Span.raw("")));
+        if (checks.isEmpty()) {
+            lines.add(center(width, Span.styled("No health checks registered", 
Theme.muted())));
+        } else if (notUp.isEmpty()) {
+            allGood(lines, info, width, now);
+        } else {
+            somethingWrong(lines, info, notUp, notReady, notLive, now);
+        }
+        lines.add(Line.from(Span.raw("")));
+        strip(lines, info, history, width, now);
+        
frame.renderWidget(Paragraph.builder().text(Text.from(lines)).block(block).build(),
 area);
+    }
+
+    /** All UP: a thumbs up, since when, how often checked, and what is 
checked. */
+    private static void allGood(List<Line> lines, IntegrationInfo info, int 
width, long now) {
+        List<HealthCheckInfo> checks = info.healthChecks;
+        lines.add(center(width, Span.styled("๐Ÿ‘  All good", 
Theme.success().bold())));
+        lines.add(Line.from(Span.raw("")));
+        lines.add(center(width,
+                Span.styled("ready ", Theme.muted()), mark(info.healthReady),
+                Span.styled("    live ", Theme.muted()), mark(info.healthLive),
+                Span.styled("    " + checks.size() + (checks.size() == 1 ? " 
check UP" : " checks UP"), Theme.muted())));
+        long since = checks.stream().mapToLong(hc -> hc.since).filter(s -> s > 
0).min().orElse(0);
+        long checked = checks.stream().mapToLong(hc -> 
hc.invocations).max().orElse(0);
+        long failures = checks.stream().mapToLong(hc -> hc.failures).sum();
+        long lastFailure = checks.stream().mapToLong(hc -> 
hc.lastFailure).max().orElse(0);
+        List<String> facts = new ArrayList<>();
+        if (since > 0) {
+            facts.add("up since " + TIME.format(Instant.ofEpochMilli(since)) + 
" (" + ago(since, now) + ")");
+        }
+        if (checked > 0) {
+            facts.add("checked " + checked + " times");
+        }
+        facts.add(failures == 0
+                ? "never failed"
+                : failures + (failures == 1 ? " failure" : " failures")
+                  + (lastFailure > 0 ? ", the last " + ago(lastFailure, now) + 
" ago" : ""));
+        lines.add(center(width, Span.styled(String.join(" ยท ", facts), 
Theme.muted())));
+        lines.add(Line.from(Span.raw("")));
+        String camel = builtInSummary(checks);
+        if (!camel.isEmpty()) {
+            lines.add(Line.from(Span.styled("   Camel checks on its own: ", 
Theme.muted()), Span.raw(camel)));
+        }
+        List<HealthCheckInfo> own = checks.stream().filter(hc -> 
!isBuiltIn(hc)).toList();
+        List<Span> yours = new ArrayList<>();
+        yours.add(Span.styled("   Your checks: ", Theme.muted()));
+        if (own.isEmpty()) {
+            yours.add(Span.styled("none", Theme.muted()));
+        }
+        for (int i = 0; i < own.size(); i++) {
+            yours.add(Span.raw(i == 0 ? "" : " ยท "));
+            yours.add(Span.styled(TuiIcons.HEALTH_UP + " " + own.get(i).name, 
Theme.success()));
+        }
+        lines.add(Line.from(yours));
+    }
+
+    /** Not all UP: the checks that are not, with why and since when, what it 
means, and the rest in one line. */
+    private static void somethingWrong(
+            List<Line> lines, IntegrationInfo info, List<HealthCheckInfo> 
notUp, boolean notReady, boolean notLive,
+            long now) {
+        for (HealthCheckInfo hc : notUp) {
+            boolean down = "DOWN".equals(hc.state);
+            Style style = down ? Theme.error() : Theme.warning();
+            List<Span> head = new ArrayList<>();
+            head.add(Span.styled("  " + (down ? TuiIcons.HEALTH_DOWN : 
TuiIcons.HEALTH_WARN) + " ", style));
+            head.add(Span.styled(hc.name != null ? hc.name : "", 
style.bold()));
+            head.add(Span.styled("   " + hc.state, style));
+            if (hc.since > 0) {
+                head.add(Span.raw(" since " + 
TIME.format(Instant.ofEpochMilli(hc.since)) + " (" + ago(hc.since, now) + ")"));
+            }
+            head.add(Span.styled(" ยท " + kind(hc), Theme.muted()));
+            lines.add(Line.from(head));
+            if (hc.message != null && !hc.message.isBlank()) {
+                lines.add(Line.from(Span.raw("      " + 
TuiHelper.fixControlChars(hc.message.lines().findFirst().orElse("")))));
+            }
+            List<String> facts = new ArrayList<>();
+            if (hc.failures > 0) {
+                facts.add("failed " + hc.failures + (hc.failures == 1 ? " 
time" : " times")
+                          + (hc.invocations > 0 ? ", of " + hc.invocations + " 
checks" : ""));
+            }
+            if (hc.lastUp > 0) {
+                facts.add("last up " + 
TIME.format(Instant.ofEpochMilli(hc.lastUp)));
+            }
+            if (!facts.isEmpty()) {
+                lines.add(Line.from(Span.styled("      " + String.join(" ยท ", 
facts), Theme.muted())));
+            }
+            lines.add(Line.from(Span.raw("")));
+        }
+        if (notLive) {
+            lines.add(Line.from(Span.styled("  โœ— not live", 
Theme.error().bold()),
+                    Span.raw(" โ€” Kubernetes would restart the integration")));
+        } else if (notReady) {
+            lines.add(Line.from(Span.styled("  live ", Theme.muted()), 
mark(info.healthLive),
+                    Span.raw(" โ€” the integration runs, but Kubernetes would 
take it out of the service until the checks"
+                             + " above are UP again")));
+        }
+        int up = info.healthChecks.size() - notUp.size();
+        if (up > 0) {
+            lines.add(Line.from(Span.styled("  " + up + (up == 1 ? " other 
check UP" : " other checks UP"),
+                    Theme.muted())));
+        }
+    }
+
+    /** Ready and live, second by second over the last minutes, as the TUI 
read them. */
+    private static void strip(List<Line> lines, IntegrationInfo info, 
HealthHistory history, int width, long now) {
+        int label = 10;
+        int seconds = Math.max(10, Math.min(HealthHistory.SECONDS, width - 
label - 24));
+        long end = now / 1000;
+        byte[] values = history.strip(info.pid, end, seconds);
+        lines.add(stripLine("ready", values, HealthHistory.READY, label, 
downNote(values, HealthHistory.READY, end)));
+        lines.add(stripLine("live", values, HealthHistory.LIVE, label, 
downNote(values, HealthHistory.LIVE, end)));
+        String from = TIME.format(Instant.ofEpochSecond(end - seconds + 1));
+        lines.add(Line.from(Span.styled(" ".repeat(label) + from
+                                        + " ".repeat(Math.max(1, seconds - 
from.length() - 3)) + "now",
+                Theme.muted())));
+    }
+
+    private static Line stripLine(String name, byte[] values, byte bit, int 
label, String note) {
+        List<Span> spans = new ArrayList<>();
+        spans.add(Span.styled(String.format("%-" + label + "s", "   " + name), 
Theme.label()));
+        for (byte v : values) {
+            if (v == HealthHistory.UNKNOWN) {
+                spans.add(Span.raw(" "));
+            } else {
+                spans.add((v & bit) != 0 ? Span.styled("โ–ˆ", Theme.success()) : 
Span.styled("โ–ˆ", Theme.error()));
+            }
+        }
+        if (note != null) {
+            spans.add(Span.styled("  " + note, Theme.warning()));
+        }
+        return Line.from(spans);
+    }
+
+    /** How long it was down in the strip and when it last was, when it was: a 
flap the page would not show. */
+    static String downNote(byte[] values, byte bit, long end) {
+        int down = 0;
+        int last = -1;
+        for (int i = 0; i < values.length; i++) {
+            if (values[i] != HealthHistory.UNKNOWN && (values[i] & bit) == 0) {
+                down++;
+                last = i;
+            }
+        }
+        if (down == 0) {
+            return null;
+        }
+        long at = end - values.length + 1 + last;
+        return "down " + down + "s, the last at " + 
TIME.format(Instant.ofEpochSecond(at));
+    }
+
+    /** Whether a check is one Camel runs on its own: the context, the routes, 
their consumers and producers. */
+    static boolean isBuiltIn(HealthCheckInfo hc) {
+        String n = hc.name;
+        if (n == null || !"camel".equals(hc.group)) {
+            return false;
+        }
+        return n.equals("context") || n.equals("route-controller") || 
n.equals("security-policy")
+                || n.equals("routes") || n.equals("consumers") || 
n.equals("producers")
+                || n.startsWith("route:") || n.startsWith("consumer:") || 
n.startsWith("producer:");
+    }
+
+    /** The checks Camel runs on its own, in words: the context, the route 
controller, 2 routes and 2 consumers. */
+    static String builtInSummary(List<HealthCheckInfo> checks) {
+        List<String> parts = new ArrayList<>();
+        int routes = 0;
+        int consumers = 0;
+        int producers = 0;
+        for (HealthCheckInfo hc : checks) {
+            if (!isBuiltIn(hc)) {
+                continue;
+            }
+            String n = hc.name;
+            if (n.startsWith("route:") || n.equals("routes")) {
+                routes++;
+            } else if (n.startsWith("consumer:") || n.equals("consumers")) {
+                consumers++;
+            } else if (n.startsWith("producer:") || n.equals("producers")) {
+                producers++;
+            } else {
+                parts.add("the " + n.replace('-', ' '));
+            }
+        }
+        if (routes > 0) {
+            parts.add(routes + (routes == 1 ? " route" : " routes"));
+        }
+        if (consumers > 0) {
+            parts.add(consumers + (consumers == 1 ? " consumer" : " 
consumers"));
+        }
+        if (producers > 0) {
+            parts.add(producers + (producers == 1 ? " producer" : " 
producers"));
+        }
+        if (parts.size() <= 1) {
+            return String.join("", parts);
+        }
+        return String.join(", ", parts.subList(0, parts.size() - 1)) + " and " 
+ parts.get(parts.size() - 1);
+    }
+
+    private static String kind(HealthCheckInfo hc) {
+        return hc.readiness && hc.liveness ? "readiness and liveness" : 
hc.liveness ? "liveness" : "readiness";
+    }
+
+    private static Span mark(Boolean ok) {
+        if (ok == null) {
+            return Span.styled("?", Theme.muted());
+        }
+        return ok ? Span.styled("โœ”", Theme.success()) : Span.styled("โœ—", 
Theme.error());
+    }
+
+    private static String ago(long time, long now) {
+        return TimeUtils.printDuration(Math.max(0, now - time), false);
+    }
+
+    private static Line center(int width, Span... spans) {
+        int w = 0;
+        for (Span s : spans) {
+            w += CharWidth.of(s.content());
+        }
+        List<Span> all = new ArrayList<>();
+        all.add(Span.raw(" ".repeat(Math.max(0, (width - w) / 2))));
+        all.addAll(List.of(spans));
+        return Line.from(all);
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HealthTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HealthTab.java
index 7de92f93a077..b3b5b995ce14 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HealthTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HealthTab.java
@@ -40,6 +40,8 @@ import static 
org.apache.camel.dsl.jbang.core.commands.tui.TuiHelper.*;
 class HealthTab extends AbstractTableTab {
 
     private boolean showOnlyDown;
+    // the page (all good, or what is wrong) unless t asks for the checks one 
by one
+    private boolean table;
 
     HealthTab(MonitorContext ctx) {
         super(ctx, "group", "name", "status");
@@ -60,7 +62,11 @@ class HealthTab extends AbstractTableTab {
 
     @Override
     protected boolean handleTabKeyEvent(KeyEvent ke) {
-        if (ke.isCharIgnoreCase('d')) {
+        if (ke.isCharIgnoreCase('t')) {
+            table = !table;
+            return true;
+        }
+        if (table && ke.isCharIgnoreCase('d')) {
             showOnlyDown = !showOnlyDown;
             return true;
         }
@@ -69,6 +75,11 @@ class HealthTab extends AbstractTableTab {
 
     @Override
     protected void renderContent(Frame frame, Rect area, IntegrationInfo info) 
{
+        if (!table) {
+            lastTableArea = null;
+            HealthPage.render(frame, area, info, ctx.healthHistory, 
System.currentTimeMillis());
+            return;
+        }
         List<HealthCheckInfo> healthChecks = new 
ArrayList<>(getFilteredHealthChecks(info));
         healthChecks.sort(this::sortHealth);
 
@@ -144,8 +155,20 @@ class HealthTab extends AbstractTableTab {
 
     @Override
     public void renderFooter(List<Span> spans) {
-        super.renderFooter(spans);
-        hint(spans, "d", showOnlyDown ? "show all" : "only DOWN");
+        if (table) {
+            super.renderFooter(spans);
+        } else {
+            // the page has nothing to sort
+            hint(spans, "Esc", "back");
+        }
+        hint(spans, "t", table ? "summary" : "all checks");
+        if (table) {
+            hint(spans, "d", showOnlyDown ? "show all" : "only DOWN");
+        }
+    }
+
+    boolean isTable() {
+        return table;
     }
 
     boolean isShowOnlyDown() {
@@ -202,6 +225,12 @@ class HealthTab extends AbstractTableTab {
         }
         JsonObject result = new JsonObject();
         result.put("tab", "Health");
+        if (info.healthReady != null) {
+            result.put("ready", info.healthReady);
+        }
+        if (info.healthLive != null) {
+            result.put("live", info.healthLive);
+        }
         JsonArray rows = new JsonArray();
         List<HealthCheckInfo> checks = new 
ArrayList<>(getFilteredHealthChecks(info));
         checks.sort(this::sortHealth);
@@ -215,6 +244,13 @@ class HealthTab extends AbstractTableTab {
             if (hi.message != null) {
                 row.put("message", hi.message);
             }
+            row.put("builtIn", HealthPage.isBuiltIn(hi));
+            if (hi.since > 0) {
+                row.put("since", hi.since);
+            }
+            if (hi.failures > 0) {
+                row.put("failures", hi.failures);
+            }
             rows.add(row);
         }
         result.put("rows", rows);
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 9879aa7d6080..4e30be288d12 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
@@ -110,6 +110,9 @@ class IntegrationInfo {
     int routeControllerExhaustedRoutes;
     final List<RouteControllerInfo> routeControllerRoutes = new ArrayList<>();
     final List<HealthCheckInfo> healthChecks = new ArrayList<>();
+    // whether the integration is ready (may take traffic) and live (runs), as 
its health checks say; null when not known
+    Boolean healthReady;
+    Boolean healthLive;
     final List<EndpointInfo> endpoints = new ArrayList<>();
     final List<ServiceInfo> services = new ArrayList<>();
     final List<CircuitBreakerInfo> circuitBreakers = new ArrayList<>();
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContext.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContext.java
index ec72b5b31007..c131107ef264 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContext.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContext.java
@@ -102,6 +102,8 @@ class MonitorContext {
     final ChaosMarks chaosMarks = new ChaosMarks();
     // what happened second by second, for the Timeline screen (CAMEL-26691)
     final Timeline timeline = new Timeline();
+    // whether the integrations were ready and live, second by second, for the 
strip of the Health tab
+    final HealthHistory healthHistory = new HealthHistory();
     BiConsumer<String, String> openMarkdownCallback;
     /** Opens markdown in the doc viewer scrolled to a heading: title, 
markdown, heading (may be null). */
     MarkdownOpener openMarkdownAtCallback;
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/PopupManager.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/PopupManager.java
index e9a2b0cb9d2e..ff70f244d3da 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/PopupManager.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/PopupManager.java
@@ -704,6 +704,13 @@ class PopupManager {
                     badge = " " + cbOpenCount + " OPEN";
                     badgeStyle = Theme.error();
                 }
+                // the health checks that are DOWN
+                long down = "Health".equals(tab.name()) && sel != null
+                        ? sel.healthChecks.stream().filter(hc -> 
"DOWN".equals(hc.state)).count() : 0;
+                if (down > 0) {
+                    badge = " " + down + " DOWN";
+                    badgeStyle = Theme.error();
+                }
                 // the chaos rules that inject faults now, and a failed verdict
                 String chaosBadge = "Chaos".equals(tab.name()) ? 
ChaosTab.badge(sel) : null;
                 if (chaosBadge != null) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusParser.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusParser.java
index 84b106f76216..b5ea0a7a4914 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusParser.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusParser.java
@@ -19,6 +19,8 @@ package org.apache.camel.dsl.jbang.core.commands.tui;
 import java.net.URI;
 import java.time.Instant;
 import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeParseException;
 import java.util.ArrayList;
 import java.util.LinkedHashMap;
 import java.util.LinkedHashSet;
@@ -380,6 +382,8 @@ final class StatusParser {
         if (healthChecks != null) {
             Boolean rdy = (Boolean) healthChecks.get("ready");
             info.ready = Boolean.TRUE.equals(rdy) ? "1/1" : "0/1";
+            info.healthReady = rdy;
+            info.healthLive = healthChecks.get("live") instanceof Boolean live 
? live : null;
             JsonArray checks = (JsonArray) healthChecks.get("checks");
             if (checks != null) {
                 for (Object c : checks) {
@@ -391,11 +395,12 @@ final class StatusParser {
                     hc.readiness = cj.getBooleanOrDefault("readiness", false);
                     hc.liveness = cj.getBooleanOrDefault("liveness", false);
                     hc.message = cj.getString("message");
-                    if (hc.message == null) {
-                        JsonObject details = (JsonObject) cj.get("details");
-                        if (details != null && 
details.containsKey("failure.error.message")) {
-                            hc.message = 
details.getString("failure.error.message");
-                        }
+                    JsonObject details = (JsonObject) cj.get("details");
+                    if (hc.message == null && details != null && 
details.containsKey("failure.error.message")) {
+                        hc.message = 
details.getString("failure.error.message");
+                    }
+                    if (details != null) {
+                        healthDetails(hc, details);
                     }
                     addHealthCheck(info.healthChecks, hc);
                 }
@@ -403,6 +408,27 @@ final class StatusParser {
         }
     }
 
+    /** When the check went to its state, when it was last UP, and how often 
it ran and failed, from its details. */
+    static void healthDetails(HealthCheckInfo hc, JsonObject details) {
+        hc.since = epochMillis(details.getString("UP".equals(hc.state) ? 
"success.start.time" : "failure.start.time"));
+        hc.lastUp = epochMillis(details.getString("success.time"));
+        hc.lastFailure = epochMillis(details.getString("failure.time"));
+        hc.invocations = TuiHelper.objToLong(details.get("invocation.count"));
+        hc.failures = TuiHelper.objToLong(details.get("failure.count"));
+    }
+
+    /** A time as the health check details have it 
(2026-11-09T17:02:42.44+01:00[Europe/Copenhagen]); 0 when not. */
+    static long epochMillis(String time) {
+        if (time == null || time.isBlank()) {
+            return 0;
+        }
+        try {
+            return ZonedDateTime.parse(time).toInstant().toEpochMilli();
+        } catch (DateTimeParseException e) {
+            return 0;
+        }
+    }
+
     /**
      * A check that runs for both readiness and liveness (context) is reported 
once per kind: one row for it, with the
      * worse state.
@@ -412,9 +438,14 @@ final class StatusParser {
             if (Objects.equals(known.group, hc.group) && 
Objects.equals(known.name, hc.name)) {
                 known.readiness |= hc.readiness;
                 known.liveness |= hc.liveness;
+                known.invocations = Math.max(known.invocations, 
hc.invocations);
+                known.failures = Math.max(known.failures, hc.failures);
+                known.lastFailure = Math.max(known.lastFailure, 
hc.lastFailure);
                 if (!"DOWN".equals(known.state) && hc.state != null && 
!"UP".equals(hc.state)) {
                     known.state = hc.state;
                     known.message = hc.message != null ? hc.message : 
known.message;
+                    known.since = hc.since;
+                    known.lastUp = hc.lastUp;
                 }
                 return;
             }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/health.md 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/health.md
index 21fdc9a45d96..4a578fcdf2c6 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/health.md
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/health.md
@@ -4,6 +4,40 @@ Health checks verify that the integration and its dependencies 
are
 functioning correctly. They are essential for container orchestration
 platforms like Kubernetes, but also useful for local monitoring.
 
+## The Page
+
+The tab opens on a page that says how the integration is in one look.
+
+When every check is UP:
+
+```
+                        ๐Ÿ‘  All good
+
+          ready โœ”    live โœ”    7 checks UP
+   up since 16:43:05 (19m) ยท checked 2329 times ยท never failed
+
+   Camel checks on its own: the context, the route controller, the security 
policy, 2 routes and 2 consumers
+   Your checks: none
+```
+
+The checks Camel runs on its own are summed up in one line; the checks
+of your own (a group other than `camel`, or a name Camel does not use)
+are named one by one, as they are the ones you wrote.
+
+When a check is not UP, the title says what it means (`NOT READY`,
+`NOT LIVE`) and the page shows those checks first: since when, the
+message, how often it failed and when it was last UP, then what it
+means for the integration (not ready: taken out of the service; not
+live: restarted), and the other checks in one line.
+
+Under both, `ready` and `live` are drawn second by second over the last
+minutes, as the TUI read them: a check that went DOWN and came back
+between two looks at the tab shows there, with how long it was down.
+
+Press `t` for the checks one by one in the table below, and `d` there
+for only the DOWN ones. The More menu has a `DOWN` badge on Health while
+a check is DOWN.
+
 ## Table Columns
 
 - **GROUP** โ€” Category of the health check: `camel` (core checks), `routes` 
(per-route health), `consumers` (consumer health)
@@ -74,4 +108,5 @@ when to restart it.
 - `Up/Down` โ€” select health check
 - `s` โ€” cycle sort column
 - `S` โ€” reverse sort order
-- `d` โ€” show only the checks that are DOWN, or all of them again
+- `t` โ€” the checks one by one in a table, or the page again
+- `d` โ€” in the table: show only the checks that are DOWN, or all of them again
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/HealthTabRenderTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/HealthTabRenderTest.java
index 2eb1fd32269e..0d0cfe888c83 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/HealthTabRenderTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/HealthTabRenderTest.java
@@ -64,7 +64,7 @@ class HealthTabRenderTest {
         hc.liveness = true;
         info.healthChecks.add(hc);
 
-        HealthTab tab = new HealthTab(ctx);
+        HealthTab tab = tableTab();
         String rendered = renderToString(tab, 100, 20);
 
         assertTrue(rendered.contains("context"), "Should render health check 
name 'context'");
@@ -82,7 +82,7 @@ class HealthTabRenderTest {
         hc.message = "Route stopped";
         info.healthChecks.add(hc);
 
-        HealthTab tab = new HealthTab(ctx);
+        HealthTab tab = tableTab();
         String rendered = renderToString(tab, 120, 20);
 
         assertTrue(rendered.contains("timer-route"), "Should render health 
check name");
@@ -99,7 +99,7 @@ class HealthTabRenderTest {
         hc.readiness = true;
         info.healthChecks.add(hc);
 
-        HealthTab tab = new HealthTab(ctx);
+        HealthTab tab = tableTab();
 
         Rect area = new Rect(0, 0, 100, 20);
         Buffer buffer = Buffer.empty(area);
@@ -136,7 +136,7 @@ class HealthTabRenderTest {
         hc.liveness = true;
         info.healthChecks.add(hc);
 
-        HealthTab tab = new HealthTab(ctx);
+        HealthTab tab = tableTab();
 
         Rect area = new Rect(0, 0, 100, 20);
         Buffer buffer = Buffer.empty(area);
@@ -166,7 +166,7 @@ class HealthTabRenderTest {
     @Test
     void renderEmptyHealthChecksShowsPlaceholder() {
         // No health checks added
-        HealthTab tab = new HealthTab(ctx);
+        HealthTab tab = tableTab();
         // Use wider terminal to avoid column truncation of the placeholder 
text
         String rendered = renderToString(tab, 140, 10);
 
@@ -180,7 +180,7 @@ class HealthTabRenderTest {
         addHealthCheck("camel", "route-controller", "UP");
         addHealthCheck("routes", "timer-route", "DOWN");
 
-        HealthTab tab = new HealthTab(ctx);
+        HealthTab tab = tableTab();
         String rendered = renderToString(tab, 120, 20);
 
         assertTrue(rendered.contains("context"), "Should render 'context' 
check");
@@ -192,7 +192,7 @@ class HealthTabRenderTest {
     void renderShowsHeaderColumns() {
         addHealthCheck("camel", "context", "UP");
 
-        HealthTab tab = new HealthTab(ctx);
+        HealthTab tab = tableTab();
         String rendered = renderToString(tab, 120, 20);
 
         assertTrue(rendered.contains("GROUP"), "Should show GROUP header");
@@ -206,7 +206,7 @@ class HealthTabRenderTest {
     void renderShowsBorderWithTitle() {
         addHealthCheck("camel", "context", "UP");
 
-        HealthTab tab = new HealthTab(ctx);
+        HealthTab tab = tableTab();
         String rendered = renderToString(tab, 120, 20);
 
         assertTrue(rendered.contains("Health"), "Should show 'Health' in the 
block title");
@@ -216,7 +216,7 @@ class HealthTabRenderTest {
     void renderNoSelectionShowsPrompt() {
         ctx.selectedPid = null;
 
-        HealthTab tab = new HealthTab(ctx);
+        HealthTab tab = tableTab();
         String rendered = renderToString(tab, 100, 10);
 
         assertTrue(rendered.contains("No integration selected") || 
rendered.contains("Select an integration"),
@@ -233,7 +233,7 @@ class HealthTabRenderTest {
         hc.liveness = true;
         info.healthChecks.add(hc);
 
-        HealthTab tab = new HealthTab(ctx);
+        HealthTab tab = tableTab();
         String rendered = renderToString(tab, 120, 20);
 
         assertTrue(rendered.contains("R/L"), "Should show R/L for 
readiness+liveness");
@@ -244,7 +244,7 @@ class HealthTabRenderTest {
         addHealthCheck("camel", "context", "UP");
         addHealthCheck("routes", "failing", "DOWN");
 
-        HealthTab tab = new HealthTab(ctx);
+        HealthTab tab = tableTab();
 
         // Press 'd' to toggle DOWN-only filter
         tab.handleKeyEvent(KeyEvent.ofChar('d', KeyModifiers.NONE));
@@ -260,7 +260,7 @@ class HealthTabRenderTest {
 
     @Test
     void renderFooterHintsContainExpectedKeys() {
-        HealthTab tab = new HealthTab(ctx);
+        HealthTab tab = tableTab();
         List<Span> footerSpans = new ArrayList<>();
         tab.renderFooter(footerSpans);
 
@@ -277,7 +277,7 @@ class HealthTabRenderTest {
     void sortCycleChangesHeaderAppearance() {
         addHealthCheck("camel", "context", "UP");
 
-        HealthTab tab = new HealthTab(ctx);
+        HealthTab tab = tableTab();
 
         // Default sort is "name" โ€” header should have a sort indicator on NAME
         String rendered1 = renderToString(tab, 120, 20);
@@ -291,8 +291,91 @@ class HealthTabRenderTest {
                 "After one sort cycle, indicator should move to STATUS");
     }
 
+    @Test
+    void allGoodIsAPageNotATableOfUp() {
+        info.healthReady = true;
+        info.healthLive = true;
+        long now = System.currentTimeMillis();
+        for (String name : List.of("context", "route-controller", 
"security-policy", "route:orders", "route:billing",
+                "consumer:orders")) {
+            HealthCheckInfo hc = check("camel", name, "UP");
+            hc.since = now - 19 * 60_000;
+            hc.invocations = 2329;
+        }
+        check("acme", "warehouse", "UP");
+
+        String rendered = renderToString(new HealthTab(ctx), 160, 30);
+
+        assertTrue(rendered.contains("All good"), rendered);
+        assertTrue(rendered.contains("7 checks UP"), rendered);
+        assertTrue(rendered.contains("checked 2329 times ยท never failed"), 
rendered);
+        assertTrue(rendered.contains(
+                "Camel checks on its own: the context, the route controller, 
the security policy, 2 routes and 1 consumer"),
+                rendered);
+        assertTrue(rendered.contains("Your checks: " + TuiIcons.HEALTH_UP + " 
warehouse"), rendered);
+        assertFalse(rendered.contains("GROUP"), "no table: " + rendered);
+    }
+
+    @Test
+    void aCheckThatIsDownComesFirstWithWhatItMeans() {
+        info.healthReady = false;
+        info.healthLive = true;
+        long now = System.currentTimeMillis();
+        check("camel", "context", "UP");
+        HealthCheckInfo down = check("camel", "consumer:orders", "DOWN");
+        down.message = "Connection refused: kafka:9092";
+        down.since = now - 4 * 60_000;
+        down.failures = 23;
+        down.invocations = 251;
+
+        String rendered = renderToString(new HealthTab(ctx), 160, 30);
+
+        assertTrue(rendered.contains("NOT READY"), rendered);
+        assertTrue(rendered.contains("consumer:orders   DOWN since"), 
rendered);
+        assertTrue(rendered.contains("Connection refused: kafka:9092"), 
rendered);
+        assertTrue(rendered.contains("failed 23 times, of 251 checks"), 
rendered);
+        assertTrue(rendered.contains("Kubernetes would take it out of the 
service"), rendered);
+        assertTrue(rendered.contains("1 other check UP"), rendered);
+    }
+
+    @Test
+    void theStripShowsAFlapThePageWouldNotShow() {
+        info.healthReady = true;
+        info.healthLive = true;
+        check("camel", "context", "UP");
+        long end = System.currentTimeMillis() / 1000;
+        for (long s = end - 60; s <= end; s++) {
+            // ready went down for 5 seconds 30 seconds ago
+            info.healthReady = s < end - 35 || s > end - 31;
+            ctx.healthHistory.record(info, s);
+        }
+        info.healthReady = true;
+
+        String rendered = renderToString(new HealthTab(ctx), 160, 30);
+
+        assertTrue(rendered.contains("All good"), rendered);
+        assertTrue(rendered.contains("down 5s, the last at"), rendered);
+    }
+
     // ---- Helper methods ----
 
+    /** The tab with the checks one by one in the table (t). */
+    private HealthTab tableTab() {
+        HealthTab tab = new HealthTab(ctx);
+        tab.handleKeyEvent(KeyEvent.ofChar('t', KeyModifiers.NONE));
+        return tab;
+    }
+
+    private HealthCheckInfo check(String group, String name, String state) {
+        HealthCheckInfo hc = new HealthCheckInfo();
+        hc.group = group;
+        hc.name = name;
+        hc.state = state;
+        hc.readiness = true;
+        info.healthChecks.add(hc);
+        return hc;
+    }
+
     private void addHealthCheck(String group, String name, String state) {
         HealthCheckInfo hc = new HealthCheckInfo();
         hc.group = group;
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusParserTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusParserTest.java
index 974a290dcd93..dc90a7f944f5 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusParserTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusParserTest.java
@@ -624,4 +624,24 @@ class StatusParserTest {
         assertEquals("C:\\new", StatusParser.bodyText("C:\\\\new"), "an 
escaped backslash stays one backslash");
         assertNull(StatusParser.bodyText(null));
     }
+
+    @Test
+    void healthDetailsSayWhenAndHowOften() {
+        HealthCheckInfo hc = new HealthCheckInfo();
+        hc.state = "UP";
+        JsonObject details = new JsonObject();
+        details.put("success.start.time", 
"2026-11-09T16:43:05.255900034+01:00[Europe/Copenhagen]");
+        details.put("success.time", 
"2026-11-09T17:02:42.440624884+01:00[Europe/Copenhagen]");
+        details.put("invocation.count", "2329");
+        details.put("failure.count", "0");
+
+        StatusParser.healthDetails(hc, details);
+
+        assertEquals(1794238985255L, hc.since);
+        assertEquals(1794240162440L, hc.lastUp);
+        assertEquals(2329, hc.invocations);
+        assertEquals(0, hc.failures);
+        assertEquals(0, hc.lastFailure);
+        assertEquals(0, StatusParser.epochMillis("not a time"));
+    }
 }

Reply via email to