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

davsclaus pushed a commit to branch feature/CAMEL-24259-tui-tabs-and-inline-docs
in repository https://gitbox.apache.org/repos/asf/camel.git

commit d5f82f5ae252f7c117ccc25b621e3b338d269334
Author: Claus Ibsen <[email protected]>
AuthorDate: Sun Jul 26 10:01:34 2026 +0200

    CAMEL-24259: camel-jbang - TUI add Producers, Events, and Route Controller 
tabs
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../camel/cli/connector/LocalCliConnector.java     |   8 +
 .../dsl/jbang/core/commands/tui/EventInfo.java     |  25 ++
 .../dsl/jbang/core/commands/tui/EventTab.java      | 234 ++++++++++++++++
 .../jbang/core/commands/tui/IntegrationInfo.java   |   7 +
 .../dsl/jbang/core/commands/tui/ProducerInfo.java  |  27 ++
 .../dsl/jbang/core/commands/tui/ProducersTab.java  | 234 ++++++++++++++++
 .../core/commands/tui/RouteControllerInfo.java     |  29 ++
 .../core/commands/tui/RouteControllerTab.java      | 293 +++++++++++++++++++++
 .../dsl/jbang/core/commands/tui/StatusParser.java  |  70 +++++
 .../dsl/jbang/core/commands/tui/TabRegistry.java   |  11 +
 .../dsl/jbang/core/commands/tui/TuiIcons.java      |   3 +
 11 files changed, 941 insertions(+)

diff --git 
a/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
 
b/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
index 290f2ec1c903..ad33fe6a3e93 100644
--- 
a/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
+++ 
b/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
@@ -1529,6 +1529,14 @@ public class LocalCliConnector extends ServiceSupport 
implements CliConnector, C
                         root.put("producers", json);
                     }
                 }
+                DevConsole dc14c = dcr.resolveById("route-controller");
+                if (dc14c != null) {
+                    JsonObject json
+                            = (JsonObject) 
dc14c.call(DevConsole.MediaType.JSON, Map.of("stacktrace", "false"));
+                    if (json != null && !json.isEmpty()) {
+                        root.put("routeController", json);
+                    }
+                }
                 DevConsole dc15 = dcr.resolveById("variables");
                 if (dc15 != null) {
                     JsonObject json = (JsonObject) 
dc15.call(DevConsole.MediaType.JSON);
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EventInfo.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EventInfo.java
new file mode 100644
index 000000000000..ed3893d11b79
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EventInfo.java
@@ -0,0 +1,25 @@
+/*
+ * 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;
+
+class EventInfo {
+    String type;
+    String category;
+    long timestamp;
+    String exchangeId;
+    String message;
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EventTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EventTab.java
new file mode 100644
index 000000000000..0a9ad6be45cc
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EventTab.java
@@ -0,0 +1,234 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import dev.tamboui.layout.Constraint;
+import dev.tamboui.layout.Rect;
+import dev.tamboui.style.Style;
+import dev.tamboui.terminal.Frame;
+import dev.tamboui.text.Span;
+import dev.tamboui.tui.event.KeyCode;
+import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.tui.event.MouseEvent;
+import dev.tamboui.widgets.block.Block;
+import dev.tamboui.widgets.block.BorderType;
+import dev.tamboui.widgets.block.Borders;
+import dev.tamboui.widgets.table.Cell;
+import dev.tamboui.widgets.table.Row;
+import dev.tamboui.widgets.table.Table;
+import org.apache.camel.util.TimeUtils;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+
+import static org.apache.camel.dsl.jbang.core.commands.tui.TuiHelper.*;
+
+class EventTab extends AbstractTableTab {
+
+    EventTab(MonitorContext ctx) {
+        super(ctx, "timestamp", "category", "type", "message");
+        this.sort = "timestamp";
+    }
+
+    @Override
+    public void navigateUp() {
+    }
+
+    @Override
+    public void navigateDown() {
+    }
+
+    @Override
+    public boolean handleKeyEvent(KeyEvent ke) {
+        if (ke.isPageUp() || ke.isKey(KeyCode.PAGE_UP)
+                || ke.isPageDown() || ke.isKey(KeyCode.PAGE_DOWN)
+                || ke.isHome() || ke.isEnd()) {
+            return false;
+        }
+        return super.handleKeyEvent(ke);
+    }
+
+    @Override
+    public boolean handleMouseEvent(MouseEvent me, Rect area) {
+        return false;
+    }
+
+    @Override
+    protected int getRowCount() {
+        IntegrationInfo info = ctx.findSelectedIntegration();
+        return info != null ? info.events.size() : 0;
+    }
+
+    @Override
+    protected void renderContent(Frame frame, Rect area, IntegrationInfo info) 
{
+        List<EventInfo> sorted = new ArrayList<>(info.events);
+        sorted.sort(this::sortEvent);
+
+        List<Row> rows = new ArrayList<>();
+        for (EventInfo ei : sorted) {
+            String ts = ei.timestamp > 0 ? TimeUtils.printSince(ei.timestamp) 
: "";
+            Style categoryStyle = categoryStyle(ei.category);
+
+            rows.add(Row.from(
+                    rightCell(ts, 12),
+                    Cell.from(Span.styled(ei.category != null ? ei.category : 
"", categoryStyle)),
+                    Cell.from(Span.styled(ei.type != null ? ei.type : "", 
Style.EMPTY.fg(Theme.accent()))),
+                    Cell.from(ei.message != null ? ei.message : "")));
+        }
+
+        if (rows.isEmpty()) {
+            rows.add(emptyRow("No events", 4));
+        }
+
+        Table table = Table.builder()
+                .rows(rows)
+                .header(Row.from(
+                        rightCell(sortLabel("AGO", "timestamp"), 12, 
sortStyle("timestamp")),
+                        Cell.from(Span.styled(sortLabel("CATEGORY", 
"category"), sortStyle("category"))),
+                        Cell.from(Span.styled(sortLabel("TYPE", "type"), 
sortStyle("type"))),
+                        Cell.from(Span.styled(sortLabel("MESSAGE", "message"), 
sortStyle("message")))))
+                .widths(
+                        Constraint.length(12),
+                        Constraint.length(12),
+                        Constraint.length(30),
+                        Constraint.fill())
+                
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
+                        .title(" Events ").build())
+                .build();
+
+        lastTableArea = area;
+        frame.renderStatefulWidget(table, area, tableState);
+        renderScrollbar(frame, sorted.size());
+    }
+
+    private int sortEvent(EventInfo a, EventInfo b) {
+        int result = switch (sort) {
+            case "category" -> {
+                String ca = a.category != null ? a.category : "";
+                String cb = b.category != null ? b.category : "";
+                yield ca.compareToIgnoreCase(cb);
+            }
+            case "type" -> {
+                String ta = a.type != null ? a.type : "";
+                String tb = b.type != null ? b.type : "";
+                yield ta.compareToIgnoreCase(tb);
+            }
+            case "message" -> {
+                String ma = a.message != null ? a.message : "";
+                String mb = b.message != null ? b.message : "";
+                yield ma.compareToIgnoreCase(mb);
+            }
+            default -> // "timestamp" — newest first by default
+                Long.compare(b.timestamp, a.timestamp);
+        };
+        return sortReversed ? -result : result;
+    }
+
+    private static Style categoryStyle(String category) {
+        if (category == null) {
+            return Style.EMPTY;
+        }
+        return switch (category) {
+            case "route" -> Style.EMPTY.fg(Theme.accent());
+            case "exchange" -> Theme.success();
+            default -> Style.EMPTY;
+        };
+    }
+
+    @Override
+    public SelectionContext getSelectionContext() {
+        IntegrationInfo info = ctx.findSelectedIntegration();
+        if (info == null || info.events.isEmpty()) {
+            return null;
+        }
+        List<EventInfo> sorted = new ArrayList<>(info.events);
+        sorted.sort(this::sortEvent);
+        List<String> items = sorted.stream().map(e -> e.type != null ? e.type 
: "").toList();
+        Integer sel = tableState.selected();
+        return new SelectionContext("table", items, sel != null ? sel : -1, 
items.size(), "Events");
+    }
+
+    @Override
+    public String description() {
+        return "Camel lifecycle events (context, route, and exchange events)";
+    }
+
+    @Override
+    public String getHelpText() {
+        return """
+                # Events
+
+                Shows Camel lifecycle events captured by the event notifier. 
Events are
+                grouped into three categories:
+
+                - **general** — Context-level events: CamelContext 
starting/started/stopping,
+                  service add/remove, component add/remove
+                - **route** — Route lifecycle events: route 
added/removed/started/stopped/reloaded
+                - **exchange** — Exchange events: exchange 
created/completed/failed/sending/sent
+
+                Events are stored in a circular buffer (default capacity 25 
per category).
+                Only the most recent events are shown.
+
+                ## Table Columns
+
+                - **AGO** — How long ago the event occurred (e.g., `2s`, 
`1m30s`)
+                - **CATEGORY** — Event category: `general`, `route`, or 
`exchange`
+                - **TYPE** — The specific event type (e.g., 
`CamelContextStartedEvent`,
+                  `RouteStartedEvent`, `ExchangeCompletedEvent`)
+                - **MESSAGE** — Human-readable event description
+
+                ## Keys
+
+                - `Up/Down` — select event
+                - `s` — cycle sort column
+                - `S` — reverse sort order
+                """;
+    }
+
+    @Override
+    public JsonObject getTableDataAsJson() {
+        IntegrationInfo info = ctx.findSelectedIntegration();
+        if (info == null) {
+            return null;
+        }
+        JsonObject result = new JsonObject();
+        result.put("tab", "Events");
+        JsonArray rows = new JsonArray();
+        List<EventInfo> sorted = new ArrayList<>(info.events);
+        sorted.sort(this::sortEvent);
+        for (EventInfo ei : sorted) {
+            JsonObject row = new JsonObject();
+            row.put("type", ei.type);
+            row.put("category", ei.category);
+            if (ei.timestamp > 0) {
+                row.put("timestamp", ei.timestamp);
+            }
+            if (ei.exchangeId != null) {
+                row.put("exchangeId", ei.exchangeId);
+            }
+            row.put("message", ei.message);
+            rows.add(row);
+        }
+        result.put("rows", rows);
+        result.put("totalRows", info.events.size());
+        Integer sel = tableState.selected();
+        result.put("selectedIndex", sel != null ? sel : -1);
+        return result;
+    }
+}
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 6598567a4198..88a2b654a942 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
@@ -80,6 +80,13 @@ class IntegrationInfo {
     long vanishStart;
     final List<RouteInfo> routes = new ArrayList<>();
     final List<ConsumerInfo> consumers = new ArrayList<>();
+    final List<ProducerInfo> producers = new ArrayList<>();
+    final List<EventInfo> events = new ArrayList<>();
+    String routeControllerType;
+    boolean routeControllerUnhealthy;
+    int routeControllerRestartingRoutes;
+    int routeControllerExhaustedRoutes;
+    final List<RouteControllerInfo> routeControllerRoutes = new ArrayList<>();
     final List<HealthCheckInfo> healthChecks = new ArrayList<>();
     final List<EndpointInfo> endpoints = 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/ProducerInfo.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProducerInfo.java
new file mode 100644
index 000000000000..e1e697163eac
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProducerInfo.java
@@ -0,0 +1,27 @@
+/*
+ * 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;
+
+class ProducerInfo {
+    String uri;
+    String state;
+    String className;
+    String routeId;
+    String stepId;
+    boolean remote;
+    boolean singleton;
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProducersTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProducersTab.java
new file mode 100644
index 000000000000..38c63698c796
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProducersTab.java
@@ -0,0 +1,234 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import dev.tamboui.layout.Constraint;
+import dev.tamboui.layout.Rect;
+import dev.tamboui.style.Style;
+import dev.tamboui.terminal.Frame;
+import dev.tamboui.text.Span;
+import dev.tamboui.tui.event.KeyCode;
+import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.tui.event.MouseEvent;
+import dev.tamboui.widgets.block.Block;
+import dev.tamboui.widgets.block.BorderType;
+import dev.tamboui.widgets.block.Borders;
+import dev.tamboui.widgets.table.Cell;
+import dev.tamboui.widgets.table.Row;
+import dev.tamboui.widgets.table.Table;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+
+import static org.apache.camel.dsl.jbang.core.commands.tui.TuiHelper.*;
+
+class ProducersTab extends AbstractTableTab {
+
+    ProducersTab(MonitorContext ctx) {
+        super(ctx, "route", "status", "type", "uri");
+    }
+
+    @Override
+    public void navigateUp() {
+    }
+
+    @Override
+    public void navigateDown() {
+    }
+
+    @Override
+    public boolean handleKeyEvent(KeyEvent ke) {
+        if (ke.isPageUp() || ke.isKey(KeyCode.PAGE_UP)
+                || ke.isPageDown() || ke.isKey(KeyCode.PAGE_DOWN)
+                || ke.isHome() || ke.isEnd()) {
+            return false;
+        }
+        return super.handleKeyEvent(ke);
+    }
+
+    @Override
+    public boolean handleMouseEvent(MouseEvent me, Rect area) {
+        return false;
+    }
+
+    @Override
+    protected int getRowCount() {
+        IntegrationInfo info = ctx.findSelectedIntegration();
+        return info != null ? info.producers.size() : 0;
+    }
+
+    @Override
+    protected void renderContent(Frame frame, Rect area, IntegrationInfo info) 
{
+        List<ProducerInfo> sorted = new ArrayList<>(info.producers);
+        sorted.sort(this::sortProducer);
+
+        List<Row> rows = new ArrayList<>();
+        for (ProducerInfo pi : sorted) {
+            String type = producerType(pi);
+            Style statusStyle = "Started".equals(pi.state) ? Theme.success() : 
Theme.error();
+
+            rows.add(Row.from(
+                    Cell.from(Span.styled(" " + (pi.routeId != null ? 
pi.routeId : ""), Style.EMPTY.fg(Theme.accent()))),
+                    Cell.from(Span.styled(pi.state != null ? pi.state : "", 
statusStyle)),
+                    Cell.from(type),
+                    Cell.from(pi.remote ? "Yes" : "No"),
+                    Cell.from(pi.uri != null ? pi.uri : "")));
+        }
+
+        if (rows.isEmpty()) {
+            rows.add(emptyRow("No producers", 5));
+        }
+
+        Table table = Table.builder()
+                .rows(rows)
+                .header(Row.from(
+                        Cell.from(Span.styled(" " + sortLabel("ROUTE", 
"route"), sortStyle("route"))),
+                        Cell.from(Span.styled(sortLabel("STATUS", "status"), 
sortStyle("status"))),
+                        Cell.from(Span.styled(sortLabel("TYPE", "type"), 
sortStyle("type"))),
+                        Cell.from(Span.styled("REMOTE", Style.EMPTY.bold())),
+                        Cell.from(Span.styled(sortLabel("URI", "uri"), 
sortStyle("uri")))))
+                .widths(
+                        Constraint.length(20),
+                        Constraint.length(10),
+                        Constraint.length(20),
+                        Constraint.length(8),
+                        Constraint.fill())
+                
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
+                        .title(" Producers ").build())
+                .build();
+
+        lastTableArea = area;
+        frame.renderStatefulWidget(table, area, tableState);
+        renderScrollbar(frame, sorted.size());
+    }
+
+    private int sortProducer(ProducerInfo a, ProducerInfo b) {
+        int result = switch (sort) {
+            case "status" -> {
+                String sa = a.state != null ? a.state : "";
+                String sb = b.state != null ? b.state : "";
+                yield sa.compareToIgnoreCase(sb);
+            }
+            case "type" -> {
+                String ta = producerType(a);
+                String tb = producerType(b);
+                yield ta.compareToIgnoreCase(tb);
+            }
+            case "uri" -> {
+                String ua = a.uri != null ? a.uri : "";
+                String ub = b.uri != null ? b.uri : "";
+                yield ua.compareToIgnoreCase(ub);
+            }
+            default -> { // "route"
+                String ra = a.routeId != null ? a.routeId : "";
+                String rb = b.routeId != null ? b.routeId : "";
+                yield ra.compareToIgnoreCase(rb);
+            }
+        };
+        return sortReversed ? -result : result;
+    }
+
+    private static String producerType(ProducerInfo pi) {
+        if (pi.className == null) {
+            return "";
+        }
+        String s = pi.className;
+        if (s.endsWith("Producer")) {
+            s = s.substring(0, s.length() - 8);
+        }
+        int dot = s.lastIndexOf('.');
+        return dot >= 0 ? s.substring(dot + 1) : s;
+    }
+
+    @Override
+    public SelectionContext getSelectionContext() {
+        IntegrationInfo info = ctx.findSelectedIntegration();
+        if (info == null || info.producers.isEmpty()) {
+            return null;
+        }
+        List<ProducerInfo> sorted = new ArrayList<>(info.producers);
+        sorted.sort(this::sortProducer);
+        List<String> items = sorted.stream().map(p -> p.routeId != null ? 
p.routeId : "").toList();
+        Integer sel = tableState.selected();
+        return new SelectionContext("table", items, sel != null ? sel : -1, 
items.size(), "Producers");
+    }
+
+    @Override
+    public String description() {
+        return "Producer statistics (output endpoints sending data to external 
systems)";
+    }
+
+    @Override
+    public String getHelpText() {
+        return """
+                # Producers
+
+                Producers are the **output** side of a Camel route. They send 
data to
+                external systems (message brokers, HTTP endpoints, databases, 
files, etc.).
+
+                Unlike consumers (one per route), a route can have multiple 
producers
+                — each `.to()` or `.toD()` call in the route creates a 
producer.
+
+                ## Table Columns
+
+                - **ROUTE** — The route this producer belongs to
+                - **STATUS** — Producer state: `Started` (running normally) or 
`Stopped`
+                - **TYPE** — The Camel component type (e.g., `Kafka`, `Http`, 
`Log`, `Seda`)
+                - **REMOTE** — Whether this producer sends to a remote system 
(`Yes`) or is in-process (`No`)
+                - **URI** — The full endpoint URI (e.g., `kafka://my-topic`, 
`log://mylogger`)
+
+                ## Keys
+
+                - `Up/Down` — select producer
+                - `s` — cycle sort column
+                - `S` — reverse sort order
+                """;
+    }
+
+    @Override
+    public JsonObject getTableDataAsJson() {
+        IntegrationInfo info = ctx.findSelectedIntegration();
+        if (info == null) {
+            return null;
+        }
+        JsonObject result = new JsonObject();
+        result.put("tab", "Producers");
+        JsonArray rows = new JsonArray();
+        List<ProducerInfo> sorted = new ArrayList<>(info.producers);
+        sorted.sort(this::sortProducer);
+        for (ProducerInfo pi : sorted) {
+            JsonObject row = new JsonObject();
+            row.put("routeId", pi.routeId);
+            row.put("uri", pi.uri);
+            row.put("state", pi.state);
+            row.put("className", pi.className);
+            row.put("remote", pi.remote);
+            row.put("singleton", pi.singleton);
+            if (pi.stepId != null) {
+                row.put("stepId", pi.stepId);
+            }
+            rows.add(row);
+        }
+        result.put("rows", rows);
+        result.put("totalRows", info.producers.size());
+        Integer sel = tableState.selected();
+        result.put("selectedIndex", sel != null ? sel : -1);
+        return result;
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RouteControllerInfo.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RouteControllerInfo.java
new file mode 100644
index 000000000000..50d9c0296234
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RouteControllerInfo.java
@@ -0,0 +1,29 @@
+/*
+ * 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;
+
+class RouteControllerInfo {
+    String routeId;
+    String status;
+    String uri;
+    String supervising;
+    long attempts;
+    long lastAttempt;
+    long nextAttempt;
+    long elapsed;
+    String error;
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RouteControllerTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RouteControllerTab.java
new file mode 100644
index 000000000000..492cfdb34ccf
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RouteControllerTab.java
@@ -0,0 +1,293 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import dev.tamboui.layout.Constraint;
+import dev.tamboui.layout.Rect;
+import dev.tamboui.style.Style;
+import dev.tamboui.terminal.Frame;
+import dev.tamboui.text.Span;
+import dev.tamboui.tui.event.KeyCode;
+import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.tui.event.MouseEvent;
+import dev.tamboui.widgets.block.Block;
+import dev.tamboui.widgets.block.BorderType;
+import dev.tamboui.widgets.block.Borders;
+import dev.tamboui.widgets.table.Cell;
+import dev.tamboui.widgets.table.Row;
+import dev.tamboui.widgets.table.Table;
+import org.apache.camel.util.TimeUtils;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+
+import static org.apache.camel.dsl.jbang.core.commands.tui.TuiHelper.*;
+
+class RouteControllerTab extends AbstractTableTab {
+
+    RouteControllerTab(MonitorContext ctx) {
+        super(ctx, "route", "status", "supervising", "attempts", "uri");
+    }
+
+    @Override
+    public void navigateUp() {
+    }
+
+    @Override
+    public void navigateDown() {
+    }
+
+    @Override
+    public boolean handleKeyEvent(KeyEvent ke) {
+        if (ke.isPageUp() || ke.isKey(KeyCode.PAGE_UP)
+                || ke.isPageDown() || ke.isKey(KeyCode.PAGE_DOWN)
+                || ke.isHome() || ke.isEnd()) {
+            return false;
+        }
+        return super.handleKeyEvent(ke);
+    }
+
+    @Override
+    public boolean handleMouseEvent(MouseEvent me, Rect area) {
+        return false;
+    }
+
+    @Override
+    protected int getRowCount() {
+        IntegrationInfo info = ctx.findSelectedIntegration();
+        return info != null ? info.routeControllerRoutes.size() : 0;
+    }
+
+    @Override
+    protected void renderContent(Frame frame, Rect area, IntegrationInfo info) 
{
+        // Handle non-supervised controller
+        if ("DefaultRouteController".equals(info.routeControllerType)) {
+            renderEmptyState(frame, area, "Route controller: Default (not 
supervised)", " Route Controller ");
+            return;
+        }
+
+        List<RouteControllerInfo> sorted = new 
ArrayList<>(info.routeControllerRoutes);
+        sorted.sort(this::sortRouteController);
+
+        // Handle supervised controller with all routes started successfully
+        if ("SupervisingRouteController".equals(info.routeControllerType)
+                && !info.routeControllerUnhealthy
+                && info.routeControllerRestartingRoutes == 0
+                && info.routeControllerExhaustedRoutes == 0
+                && sorted.stream().allMatch(r -> "Started".equals(r.status))) {
+            renderEmptyState(frame, area, "All routes started successfully", " 
Route Controller (Supervised) ");
+            return;
+        }
+
+        List<Row> rows = new ArrayList<>();
+        for (RouteControllerInfo rc : sorted) {
+            Style statusStyle = "Started".equals(rc.status) ? Theme.success() 
: Theme.error();
+            String supervising = rc.supervising != null ? rc.supervising : "";
+            String attempts = rc.attempts > 0 ? String.valueOf(rc.attempts) : 
"";
+            String lastAttempt = rc.lastAttempt > 0 ? 
TimeUtils.printSince(rc.lastAttempt) : "";
+            String nextAttempt = rc.nextAttempt > 0 ? 
TimeUtils.printSince(rc.nextAttempt) : "";
+            String uriOrError = rc.error != null ? rc.error : (rc.uri != null 
? rc.uri : "");
+            Style uriStyle = rc.error != null ? Theme.error() : Style.EMPTY;
+
+            rows.add(Row.from(
+                    Cell.from(Span.styled(" " + (rc.routeId != null ? 
rc.routeId : ""), Style.EMPTY.fg(Theme.accent()))),
+                    Cell.from(Span.styled(rc.status != null ? rc.status : "", 
statusStyle)),
+                    Cell.from(supervising),
+                    rightCell(attempts, 10),
+                    rightCell(lastAttempt, 12),
+                    rightCell(nextAttempt, 12),
+                    Cell.from(Span.styled(uriOrError, uriStyle))));
+        }
+
+        if (rows.isEmpty()) {
+            rows.add(emptyRow("No routes under supervision", 7));
+        }
+
+        String title = 
"SupervisingRouteController".equals(info.routeControllerType)
+                ? " Route Controller (Supervised) "
+                : " Route Controller ";
+
+        Table table = Table.builder()
+                .rows(rows)
+                .header(Row.from(
+                        Cell.from(Span.styled(" " + sortLabel("ROUTE", 
"route"), sortStyle("route"))),
+                        Cell.from(Span.styled(sortLabel("STATUS", "status"), 
sortStyle("status"))),
+                        Cell.from(Span.styled(sortLabel("SUPERVISING", 
"supervising"), sortStyle("supervising"))),
+                        rightCell(sortLabel("ATTEMPTS", "attempts"), 10, 
sortStyle("attempts")),
+                        rightCell("LAST", 12, Style.EMPTY.bold()),
+                        rightCell("NEXT", 12, Style.EMPTY.bold()),
+                        Cell.from(Span.styled(sortLabel("URI", "uri"), 
sortStyle("uri")))))
+                .widths(
+                        Constraint.length(20),
+                        Constraint.length(10),
+                        Constraint.length(14),
+                        Constraint.length(10),
+                        Constraint.length(12),
+                        Constraint.length(12),
+                        Constraint.fill())
+                
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
+                        .title(title).build())
+                .build();
+
+        lastTableArea = area;
+        frame.renderStatefulWidget(table, area, tableState);
+        renderScrollbar(frame, sorted.size());
+    }
+
+    private void renderEmptyState(Frame frame, Rect area, String message, 
String title) {
+        List<Row> rows = new ArrayList<>();
+        rows.add(Row.from(Cell.from(Span.styled(" " + message, 
Theme.muted()))));
+
+        Table table = Table.builder()
+                .rows(rows)
+                .header(Row.from(Cell.from(Span.styled(" INFO", 
Style.EMPTY.bold()))))
+                .widths(Constraint.fill())
+                
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
+                        .title(title).build())
+                .build();
+
+        lastTableArea = area;
+        frame.renderStatefulWidget(table, area, tableState);
+    }
+
+    private int sortRouteController(RouteControllerInfo a, RouteControllerInfo 
b) {
+        int result = switch (sort) {
+            case "status" -> {
+                String sa = a.status != null ? a.status : "";
+                String sb = b.status != null ? b.status : "";
+                yield sa.compareToIgnoreCase(sb);
+            }
+            case "supervising" -> {
+                String sa = a.supervising != null ? a.supervising : "";
+                String sb = b.supervising != null ? b.supervising : "";
+                yield sa.compareToIgnoreCase(sb);
+            }
+            case "attempts" -> Long.compare(b.attempts, a.attempts);
+            case "uri" -> {
+                String ua = a.uri != null ? a.uri : "";
+                String ub = b.uri != null ? b.uri : "";
+                yield ua.compareToIgnoreCase(ub);
+            }
+            default -> { // "route"
+                String ra = a.routeId != null ? a.routeId : "";
+                String rb = b.routeId != null ? b.routeId : "";
+                yield ra.compareToIgnoreCase(rb);
+            }
+        };
+        return sortReversed ? -result : result;
+    }
+
+    @Override
+    public SelectionContext getSelectionContext() {
+        IntegrationInfo info = ctx.findSelectedIntegration();
+        if (info == null || info.routeControllerRoutes.isEmpty()) {
+            return null;
+        }
+        List<RouteControllerInfo> sorted = new 
ArrayList<>(info.routeControllerRoutes);
+        sorted.sort(this::sortRouteController);
+        List<String> items = sorted.stream().map(r -> r.routeId != null ? 
r.routeId : "").toList();
+        Integer sel = tableState.selected();
+        return new SelectionContext("table", items, sel != null ? sel : -1, 
items.size(), "Route Controller");
+    }
+
+    @Override
+    public String description() {
+        return "Route controller status (supervised route startup and restart 
attempts)";
+    }
+
+    @Override
+    public String getHelpText() {
+        return """
+                # Route Controller
+
+                Shows the status of the Camel route controller. The route 
controller manages
+                route startup and can automatically restart routes that fail 
to start.
+
+                There are two types of route controllers:
+
+                - **Default** — Routes are started in order and a failure 
stops the context.
+                  This tab shows "Route controller: Default (not supervised)" 
in this case.
+                - **Supervising** — Routes that fail to start are retried with 
exponential
+                  backoff. The controller tracks attempts and manages restart 
scheduling.
+
+                When using the supervised controller and all routes start 
successfully,
+                the tab shows "All routes started successfully".
+
+                ## Table Columns
+
+                - **ROUTE** — The route ID
+                - **STATUS** — Route state: `Started`, `Stopped`, or other 
lifecycle states
+                - **SUPERVISING** — Supervision status (e.g., `Active` when 
the route is being
+                  restarted by the controller)
+                - **ATTEMPTS** — Number of restart attempts so far
+                - **LAST** — Time since the last restart attempt
+                - **NEXT** — Time until the next scheduled restart attempt
+                - **URI** — The route's consumer endpoint URI. If the route 
has a startup error,
+                  the error message is shown here instead (in red)
+
+                ## Keys
+
+                - `Up/Down` — select route
+                - `s` — cycle sort column
+                - `S` — reverse sort order
+                """;
+    }
+
+    @Override
+    public JsonObject getTableDataAsJson() {
+        IntegrationInfo info = ctx.findSelectedIntegration();
+        if (info == null) {
+            return null;
+        }
+        JsonObject result = new JsonObject();
+        result.put("tab", "Route Controller");
+        result.put("controllerType", info.routeControllerType);
+        result.put("unhealthy", info.routeControllerUnhealthy);
+        result.put("restartingRoutes", info.routeControllerRestartingRoutes);
+        result.put("exhaustedRoutes", info.routeControllerExhaustedRoutes);
+        JsonArray rows = new JsonArray();
+        List<RouteControllerInfo> sorted = new 
ArrayList<>(info.routeControllerRoutes);
+        sorted.sort(this::sortRouteController);
+        for (RouteControllerInfo rc : sorted) {
+            JsonObject row = new JsonObject();
+            row.put("routeId", rc.routeId);
+            row.put("status", rc.status);
+            row.put("uri", rc.uri);
+            if (rc.supervising != null) {
+                row.put("supervising", rc.supervising);
+            }
+            row.put("attempts", rc.attempts);
+            if (rc.lastAttempt > 0) {
+                row.put("lastAttempt", rc.lastAttempt);
+            }
+            if (rc.nextAttempt > 0) {
+                row.put("nextAttempt", rc.nextAttempt);
+            }
+            if (rc.error != null) {
+                row.put("error", rc.error);
+            }
+            rows.add(row);
+        }
+        result.put("rows", rows);
+        result.put("totalRows", info.routeControllerRoutes.size());
+        Integer sel = tableState.selected();
+        result.put("selectedIndex", sel != null ? sel : -1);
+        return result;
+    }
+}
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 f0a3be50cd7a..db04860676c5 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
@@ -326,6 +326,60 @@ final class StatusParser {
             }
         }
 
+        // Parse producers
+        JsonObject producersObj = (JsonObject) root.get("producers");
+        if (producersObj != null) {
+            JsonArray producerList = (JsonArray) producersObj.get("producers");
+            if (producerList != null) {
+                for (Object p : producerList) {
+                    JsonObject pj = (JsonObject) p;
+                    ProducerInfo pi = new ProducerInfo();
+                    pi.uri = pj.getString("uri");
+                    pi.state = pj.getString("state");
+                    pi.className = pj.getString("class");
+                    pi.routeId = pj.getString("routeId");
+                    pi.stepId = pj.getString("stepId");
+                    pi.remote = Boolean.TRUE.equals(pj.get("remote"));
+                    pi.singleton = Boolean.TRUE.equals(pj.get("singleton"));
+                    info.producers.add(pi);
+                }
+            }
+        }
+
+        // Parse events
+        JsonObject eventsObj = (JsonObject) root.get("events");
+        if (eventsObj != null) {
+            parseEventArray(eventsObj, "events", "general", info);
+            parseEventArray(eventsObj, "routeEvents", "route", info);
+            parseEventArray(eventsObj, "exchangeEvents", "exchange", info);
+        }
+
+        // Parse route controller
+        JsonObject rcObj = (JsonObject) root.get("routeController");
+        if (rcObj != null) {
+            info.routeControllerType = rcObj.getString("controller");
+            info.routeControllerUnhealthy = 
Boolean.TRUE.equals(rcObj.get("unhealthyRoutes"));
+            info.routeControllerRestartingRoutes = 
rcObj.getIntegerOrDefault("restartingRoutes", 0);
+            info.routeControllerExhaustedRoutes = 
rcObj.getIntegerOrDefault("exhaustedRoutes", 0);
+            JsonArray rcRoutes = (JsonArray) rcObj.get("routes");
+            if (rcRoutes != null) {
+                for (Object r : rcRoutes) {
+                    JsonObject rj = (JsonObject) r;
+                    RouteControllerInfo rc = new RouteControllerInfo();
+                    rc.routeId = rj.getString("routeId");
+                    rc.status = rj.getString("status");
+                    rc.uri = rj.getString("uri");
+                    rc.supervising = rj.getString("supervising");
+                    rc.attempts = rj.getLongOrDefault("attempts", 0);
+                    rc.lastAttempt = rj.getLongOrDefault("lastAttempt", 0);
+                    rc.nextAttempt = rj.getLongOrDefault("nextAttempt", 0);
+                    rc.elapsed = rj.getLongOrDefault("elapsed", 0);
+                    rc.error = rj.getString("error");
+                    info.routeControllerRoutes.add(rc);
+                }
+            }
+        }
+
         // Parse endpoints (top-level "endpoints" is a JsonObject with nested 
"endpoints" array)
         JsonObject endpointsObj = (JsonObject) root.get("endpoints");
         if (endpointsObj != null) {
@@ -1220,4 +1274,20 @@ final class StatusParser {
     static long objToLong(Object o) {
         return TuiHelper.objToLong(o);
     }
+
+    private static void parseEventArray(JsonObject eventsObj, String arrayKey, 
String category, IntegrationInfo info) {
+        JsonArray arr = (JsonArray) eventsObj.get(arrayKey);
+        if (arr != null) {
+            for (Object e : arr) {
+                JsonObject ej = (JsonObject) e;
+                EventInfo ei = new EventInfo();
+                ei.type = ej.getString("type");
+                ei.category = category;
+                ei.timestamp = ej.getLongOrDefault("timestamp", 0);
+                ei.exchangeId = ej.getString("exchangeId");
+                ei.message = ej.getString("message");
+                info.events.add(ei);
+            }
+        }
+    }
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
index ab90e81eece4..4ddbc11d3989 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
@@ -70,6 +70,9 @@ class TabRegistry {
     private DiagramTab diagramTab;
     private RoutesTab routesTab;
     private ConsumersTab consumersTab;
+    private ProducersTab producersTab;
+    private EventTab eventTab;
+    private RouteControllerTab routeControllerTab;
     private EndpointsTab endpointsTab;
     private HttpTab httpTab;
     private HealthTab healthTab;
@@ -115,6 +118,9 @@ class TabRegistry {
         diagramTab = new DiagramTab(ctx);
         routesTab = new RoutesTab(ctx);
         consumersTab = new ConsumersTab(ctx);
+        producersTab = new ProducersTab(ctx);
+        eventTab = new EventTab(ctx);
+        routeControllerTab = new RouteControllerTab(ctx);
         kafkaTab = new KafkaTab(ctx);
         dataSourceTab = new DataSourceTab(ctx);
         heapHistogramTab = new HeapHistogramTab(ctx);
@@ -160,9 +166,14 @@ class TabRegistry {
                 new MoreTab(TuiIcons.TAB_CIRCUIT_BREAKER, "Circuit Breaker", 
"&Circuit Breaker", circuitBreakerTab, "Routing"),
                 new MoreTab(TuiIcons.TAB_CONSUMERS, "Consumers", "Co&nsumers", 
consumersTab, "Routing"),
                 new MoreTab(TuiIcons.TAB_INFLIGHT, "Inflight", "In&flight", 
inflightTab, "Routing"),
+                new MoreTab(TuiIcons.TAB_PRODUCERS, "Producers", "Prod&ucers", 
producersTab, "Routing"),
+                new MoreTab(
+                        TuiIcons.TAB_ROUTE_CONTROLLER, "Route Controller", 
"Route Contr&oller", routeControllerTab,
+                        "Routing"),
                 // Observability
                 new MoreTab(TuiIcons.TAB_HEALTH, "Health", "H&ealth", 
healthTab, "Observability"),
                 new MoreTab(TuiIcons.TAB_METRICS, "Metrics", "Metr&ics", 
metricsTab, "Observability"),
+                new MoreTab(TuiIcons.TAB_EVENTS, "Events", "&Events", 
eventTab, "Observability"),
                 new MoreTab(TuiIcons.TAB_SPANS, "Spans", "&OTel Spans", 
spansTab, "Observability"),
                 // Data
                 new MoreTab(TuiIcons.TAB_DATASOURCE, "JDBC DataSource", "&JDBC 
DataSource", dataSourceTab, "Data"),
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIcons.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIcons.java
index 395d0a118149..ad41009894f1 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIcons.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIcons.java
@@ -146,6 +146,9 @@ final class TuiIcons {
     static final String TAB_PROCESS = CLIPBOARD;
     static final String TAB_STARTUP = QUARKUS;
     static final String TAB_THREADS = "🧵";
+    static final String TAB_PRODUCERS = "📤";
+    static final String TAB_EVENTS = "📣";
+    static final String TAB_ROUTE_CONTROLLER = "🚦";
 
     /** Icons for {@link TabRegistry#TAB_OVERVIEW}..{@link 
TabRegistry#TAB_MORE} (in order). */
     static final List<String> PRIMARY_TAB_ICONS = List.of(


Reply via email to