atiaomar1978-hub commented on code in PR #25317:
URL: https://github.com/apache/camel/pull/25317#discussion_r3714423977


##########
dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiWebServer.java:
##########
@@ -0,0 +1,198 @@
+/*
+ * 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.io.IOException;
+import java.lang.System.Logger;
+import java.lang.System.Logger.Level;
+import java.net.BindException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+import dev.tamboui.backend.aesh.AeshBackend;
+import io.netty.bootstrap.ServerBootstrap;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInitializer;
+import io.netty.channel.ChannelPipeline;
+import io.netty.channel.EventLoopGroup;
+import io.netty.channel.MultiThreadIoEventLoopGroup;
+import io.netty.channel.SimpleChannelInboundHandler;
+import io.netty.channel.group.ChannelGroup;
+import io.netty.channel.group.DefaultChannelGroup;
+import io.netty.channel.nio.NioIoHandler;
+import io.netty.channel.socket.SocketChannel;
+import io.netty.channel.socket.nio.NioServerSocketChannel;
+import io.netty.handler.codec.http.DefaultFullHttpResponse;
+import io.netty.handler.codec.http.FullHttpRequest;
+import io.netty.handler.codec.http.HttpHeaderNames;
+import io.netty.handler.codec.http.HttpObjectAggregator;
+import io.netty.handler.codec.http.HttpResponseStatus;
+import io.netty.handler.codec.http.HttpServerCodec;
+import io.netty.handler.codec.http.HttpVersion;
+import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
+import io.netty.handler.stream.ChunkedWriteHandler;
+import io.netty.util.concurrent.ImmediateEventExecutor;
+import org.aesh.terminal.Connection;
+import org.aesh.terminal.http.netty.HttpRequestHandler;
+import org.aesh.terminal.http.netty.TtyWebSocketFrameHandler;
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+
+/**
+ * Serves the Camel TUI dashboard to a web browser over WebSocket, using 
Aesh's HTTP/WebSocket terminal bridge.
+ * <p>
+ * Each incoming connection gets its own {@link CamelMonitor} instance 
(running the same live-monitoring logic as a
+ * local terminal session) driven by an {@link AeshBackend} wrapping that 
connection.
+ * <p>
+ * Binds to 127.0.0.1 only for security.
+ */
+class TuiWebServer {
+
+    private static final Logger LOG = 
System.getLogger(TuiWebServer.class.getName());
+    private final int port;
+    private final CamelJBangMain main;
+    private final ClassLoader classLoader;
+    private final String name;
+    private final long refreshInterval;
+    private final String theme;
+    private final ChannelGroup channels = new 
DefaultChannelGroup(ImmediateEventExecutor.INSTANCE);
+    private final EventLoopGroup bossGroup = new 
MultiThreadIoEventLoopGroup(1, NioIoHandler.newFactory());
+    private final EventLoopGroup workerGroup = new 
MultiThreadIoEventLoopGroup(NioIoHandler.newFactory());
+    private final ExecutorService sessionExecutor = 
Executors.newFixedThreadPool(
+            Math.max(4, Runtime.getRuntime().availableProcessors() * 2), r -> {
+                Thread t = new Thread(r, "tui-web-session");
+                t.setDaemon(true);
+                return t;
+            });
+    private Channel serverChannel;
+    private boolean stopped;
+
+    TuiWebServer(int port, CamelJBangMain main, ClassLoader classLoader, 
String name, long refreshInterval,
+                 String theme) {
+        this.port = port;
+        this.main = main;
+        this.classLoader = classLoader;
+        this.name = name;
+        this.refreshInterval = refreshInterval;
+        this.theme = theme;
+    }
+
+    void start() throws IOException {
+        try {
+            ServerBootstrap bootstrap = new ServerBootstrap();
+            serverChannel = bootstrap.group(bossGroup, workerGroup)
+                    .channel(NioServerSocketChannel.class)
+                    .childHandler(new WebServerInitializer())
+                    .bind("127.0.0.1", port)
+                    .sync()
+                    .channel();
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            stop();
+            throw new IOException("Interrupted while starting web terminal 
server", e);
+        } catch (Exception e) {
+            stop();
+            Throwable cause = e.getCause();
+            if (cause instanceof BindException bindException) {
+                throw bindException;
+            }
+            if (e instanceof BindException bindException) {
+                throw bindException;
+            }
+            throw new IOException("Failed to start web terminal server", e);
+        }
+    }
+
+    synchronized void stop() {
+        if (stopped) {
+            return;
+        }
+        stopped = true;
+        if (serverChannel != null) {
+            serverChannel.close().syncUninterruptibly();
+        }
+        channels.close().syncUninterruptibly();
+        bossGroup.shutdownGracefully(0, 5, 
TimeUnit.SECONDS).syncUninterruptibly();
+        workerGroup.shutdownGracefully(0, 5, 
TimeUnit.SECONDS).syncUninterruptibly();
+        sessionExecutor.shutdownNow();

Review Comment:
   **[Medium — BugBot]** `stop()` calls `sessionExecutor.shutdownNow()` 
immediately after closing Netty channels, without waiting for in-flight 
`accept()` tasks to finish. When the local terminal exits while browser tabs 
are connected, `CamelMonitor.call()` `finally` blocks can race with event-loop 
teardown and be interrupted mid-cleanup.
   
   **Suggestion:** Close channels first, then `shutdown()` + `awaitTermination` 
on `sessionExecutor` (with a timeout) before shutting down Netty groups; only 
then `shutdownNow()` as a last resort.



##########
dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiWebServer.java:
##########
@@ -0,0 +1,198 @@
+/*
+ * 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.io.IOException;
+import java.lang.System.Logger;
+import java.lang.System.Logger.Level;
+import java.net.BindException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+import dev.tamboui.backend.aesh.AeshBackend;
+import io.netty.bootstrap.ServerBootstrap;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInitializer;
+import io.netty.channel.ChannelPipeline;
+import io.netty.channel.EventLoopGroup;
+import io.netty.channel.MultiThreadIoEventLoopGroup;
+import io.netty.channel.SimpleChannelInboundHandler;
+import io.netty.channel.group.ChannelGroup;
+import io.netty.channel.group.DefaultChannelGroup;
+import io.netty.channel.nio.NioIoHandler;
+import io.netty.channel.socket.SocketChannel;
+import io.netty.channel.socket.nio.NioServerSocketChannel;
+import io.netty.handler.codec.http.DefaultFullHttpResponse;
+import io.netty.handler.codec.http.FullHttpRequest;
+import io.netty.handler.codec.http.HttpHeaderNames;
+import io.netty.handler.codec.http.HttpObjectAggregator;
+import io.netty.handler.codec.http.HttpResponseStatus;
+import io.netty.handler.codec.http.HttpServerCodec;
+import io.netty.handler.codec.http.HttpVersion;
+import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
+import io.netty.handler.stream.ChunkedWriteHandler;
+import io.netty.util.concurrent.ImmediateEventExecutor;
+import org.aesh.terminal.Connection;
+import org.aesh.terminal.http.netty.HttpRequestHandler;
+import org.aesh.terminal.http.netty.TtyWebSocketFrameHandler;
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+
+/**
+ * Serves the Camel TUI dashboard to a web browser over WebSocket, using 
Aesh's HTTP/WebSocket terminal bridge.
+ * <p>
+ * Each incoming connection gets its own {@link CamelMonitor} instance 
(running the same live-monitoring logic as a
+ * local terminal session) driven by an {@link AeshBackend} wrapping that 
connection.
+ * <p>
+ * Binds to 127.0.0.1 only for security.
+ */
+class TuiWebServer {
+
+    private static final Logger LOG = 
System.getLogger(TuiWebServer.class.getName());
+    private final int port;
+    private final CamelJBangMain main;
+    private final ClassLoader classLoader;
+    private final String name;
+    private final long refreshInterval;
+    private final String theme;
+    private final ChannelGroup channels = new 
DefaultChannelGroup(ImmediateEventExecutor.INSTANCE);
+    private final EventLoopGroup bossGroup = new 
MultiThreadIoEventLoopGroup(1, NioIoHandler.newFactory());
+    private final EventLoopGroup workerGroup = new 
MultiThreadIoEventLoopGroup(NioIoHandler.newFactory());
+    private final ExecutorService sessionExecutor = 
Executors.newFixedThreadPool(
+            Math.max(4, Runtime.getRuntime().availableProcessors() * 2), r -> {
+                Thread t = new Thread(r, "tui-web-session");
+                t.setDaemon(true);
+                return t;
+            });
+    private Channel serverChannel;
+    private boolean stopped;
+
+    TuiWebServer(int port, CamelJBangMain main, ClassLoader classLoader, 
String name, long refreshInterval,
+                 String theme) {
+        this.port = port;
+        this.main = main;
+        this.classLoader = classLoader;
+        this.name = name;
+        this.refreshInterval = refreshInterval;
+        this.theme = theme;
+    }
+
+    void start() throws IOException {
+        try {
+            ServerBootstrap bootstrap = new ServerBootstrap();
+            serverChannel = bootstrap.group(bossGroup, workerGroup)
+                    .channel(NioServerSocketChannel.class)
+                    .childHandler(new WebServerInitializer())
+                    .bind("127.0.0.1", port)
+                    .sync()
+                    .channel();
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            stop();
+            throw new IOException("Interrupted while starting web terminal 
server", e);
+        } catch (Exception e) {
+            stop();
+            Throwable cause = e.getCause();
+            if (cause instanceof BindException bindException) {
+                throw bindException;
+            }
+            if (e instanceof BindException bindException) {
+                throw bindException;
+            }
+            throw new IOException("Failed to start web terminal server", e);
+        }
+    }
+
+    synchronized void stop() {
+        if (stopped) {
+            return;
+        }
+        stopped = true;
+        if (serverChannel != null) {
+            serverChannel.close().syncUninterruptibly();
+        }
+        channels.close().syncUninterruptibly();
+        bossGroup.shutdownGracefully(0, 5, 
TimeUnit.SECONDS).syncUninterruptibly();
+        workerGroup.shutdownGracefully(0, 5, 
TimeUnit.SECONDS).syncUninterruptibly();
+        sessionExecutor.shutdownNow();
+    }
+
+    boolean awaitTermination(long timeout, TimeUnit unit) throws 
InterruptedException {
+        long deadline = System.nanoTime() + unit.toNanos(timeout);
+        return bossGroup.terminationFuture().await(timeout, unit)
+                && workerGroup.terminationFuture().await(timeout, unit)
+                && sessionExecutor.awaitTermination(Math.max(0, deadline - 
System.nanoTime()), TimeUnit.NANOSECONDS);
+    }
+
+    private void accept(Connection connection) {
+        sessionExecutor.submit(() -> {
+            try {
+                AeshBackend backend = new AeshBackend(connection);
+                CamelMonitor monitor = new CamelMonitor(main, classLoader);
+                monitor.name = name;
+                monitor.refreshInterval = refreshInterval;
+                monitor.theme = theme;
+                monitor.webBackend = backend;
+                monitor.call();
+            } catch (Exception e) {
+                LOG.log(Level.WARNING, "Web TUI session ended with an error", 
e);
+            } finally {
+                try {
+                    connection.close();
+                } catch (Exception ignored) {
+                    // connection already closing
+                }
+            }
+        });
+    }
+
+    private final class WebServerInitializer extends 
ChannelInitializer<SocketChannel> {
+
+        @Override
+        protected void initChannel(SocketChannel channel) {
+            ChannelPipeline pipeline = channel.pipeline();
+            pipeline.addLast(new HttpServerCodec());
+            pipeline.addLast(new ChunkedWriteHandler());
+            pipeline.addLast(new HttpObjectAggregator(65_536));
+            pipeline.addLast(new OriginCheckingUpgradeHandler());
+            pipeline.addLast(new HttpRequestHandler("/ws", "/tui/web"));
+            pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));
+            pipeline.addLast(new TtyWebSocketFrameHandler(channels, 
TuiWebServer.this::accept));
+        }
+    }
+
+    private final class OriginCheckingUpgradeHandler extends 
SimpleChannelInboundHandler<FullHttpRequest> {
+
+        @Override
+        protected void channelRead0(ChannelHandlerContext context, 
FullHttpRequest request) {
+            if ("/ws".equalsIgnoreCase(request.uri()) && 
!isAllowedOrigin(request)) {
+                context.writeAndFlush(new 
DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.FORBIDDEN))
+                        .addListener(future -> context.close());
+                return;
+            }
+            context.fireChannelRead(request.retain());
+        }
+    }
+
+    private boolean isAllowedOrigin(FullHttpRequest request) {
+        String origin = request.headers().get(HttpHeaderNames.ORIGIN);
+        // A missing Origin header is allowed on purpose: non-browser clients 
(curl, custom
+        // terminal clients) don't send one, and the loopback-only bind is the 
actual boundary here.
+        return origin == null || origin.equals("http://127.0.0.1:"; + port) || 
origin.equals("http://localhost:"; + port);

Review Comment:
   **[Info]** Missing `Origin` is intentionally allowed for non-browser clients 
(curl, tests). This is consistent with the security-model doc (loopback bind is 
the trust boundary), but worth noting: **any local process** can open a 
WebSocket session without origin validation.



##########
dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiWebServer.java:
##########
@@ -0,0 +1,198 @@
+/*
+ * 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.io.IOException;
+import java.lang.System.Logger;
+import java.lang.System.Logger.Level;
+import java.net.BindException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+import dev.tamboui.backend.aesh.AeshBackend;
+import io.netty.bootstrap.ServerBootstrap;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInitializer;
+import io.netty.channel.ChannelPipeline;
+import io.netty.channel.EventLoopGroup;
+import io.netty.channel.MultiThreadIoEventLoopGroup;
+import io.netty.channel.SimpleChannelInboundHandler;
+import io.netty.channel.group.ChannelGroup;
+import io.netty.channel.group.DefaultChannelGroup;
+import io.netty.channel.nio.NioIoHandler;
+import io.netty.channel.socket.SocketChannel;
+import io.netty.channel.socket.nio.NioServerSocketChannel;
+import io.netty.handler.codec.http.DefaultFullHttpResponse;
+import io.netty.handler.codec.http.FullHttpRequest;
+import io.netty.handler.codec.http.HttpHeaderNames;
+import io.netty.handler.codec.http.HttpObjectAggregator;
+import io.netty.handler.codec.http.HttpResponseStatus;
+import io.netty.handler.codec.http.HttpServerCodec;
+import io.netty.handler.codec.http.HttpVersion;
+import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
+import io.netty.handler.stream.ChunkedWriteHandler;
+import io.netty.util.concurrent.ImmediateEventExecutor;
+import org.aesh.terminal.Connection;
+import org.aesh.terminal.http.netty.HttpRequestHandler;
+import org.aesh.terminal.http.netty.TtyWebSocketFrameHandler;
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+
+/**
+ * Serves the Camel TUI dashboard to a web browser over WebSocket, using 
Aesh's HTTP/WebSocket terminal bridge.
+ * <p>
+ * Each incoming connection gets its own {@link CamelMonitor} instance 
(running the same live-monitoring logic as a
+ * local terminal session) driven by an {@link AeshBackend} wrapping that 
connection.
+ * <p>
+ * Binds to 127.0.0.1 only for security.
+ */
+class TuiWebServer {
+
+    private static final Logger LOG = 
System.getLogger(TuiWebServer.class.getName());
+    private final int port;
+    private final CamelJBangMain main;
+    private final ClassLoader classLoader;
+    private final String name;
+    private final long refreshInterval;
+    private final String theme;
+    private final ChannelGroup channels = new 
DefaultChannelGroup(ImmediateEventExecutor.INSTANCE);
+    private final EventLoopGroup bossGroup = new 
MultiThreadIoEventLoopGroup(1, NioIoHandler.newFactory());
+    private final EventLoopGroup workerGroup = new 
MultiThreadIoEventLoopGroup(NioIoHandler.newFactory());
+    private final ExecutorService sessionExecutor = 
Executors.newFixedThreadPool(
+            Math.max(4, Runtime.getRuntime().availableProcessors() * 2), r -> {
+                Thread t = new Thread(r, "tui-web-session");
+                t.setDaemon(true);
+                return t;
+            });
+    private Channel serverChannel;
+    private boolean stopped;
+
+    TuiWebServer(int port, CamelJBangMain main, ClassLoader classLoader, 
String name, long refreshInterval,
+                 String theme) {
+        this.port = port;
+        this.main = main;
+        this.classLoader = classLoader;
+        this.name = name;
+        this.refreshInterval = refreshInterval;
+        this.theme = theme;
+    }
+
+    void start() throws IOException {
+        try {
+            ServerBootstrap bootstrap = new ServerBootstrap();
+            serverChannel = bootstrap.group(bossGroup, workerGroup)
+                    .channel(NioServerSocketChannel.class)
+                    .childHandler(new WebServerInitializer())
+                    .bind("127.0.0.1", port)
+                    .sync()
+                    .channel();
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            stop();
+            throw new IOException("Interrupted while starting web terminal 
server", e);
+        } catch (Exception e) {
+            stop();
+            Throwable cause = e.getCause();
+            if (cause instanceof BindException bindException) {
+                throw bindException;
+            }
+            if (e instanceof BindException bindException) {
+                throw bindException;
+            }
+            throw new IOException("Failed to start web terminal server", e);
+        }
+    }
+
+    synchronized void stop() {
+        if (stopped) {
+            return;
+        }
+        stopped = true;
+        if (serverChannel != null) {
+            serverChannel.close().syncUninterruptibly();
+        }
+        channels.close().syncUninterruptibly();
+        bossGroup.shutdownGracefully(0, 5, 
TimeUnit.SECONDS).syncUninterruptibly();
+        workerGroup.shutdownGracefully(0, 5, 
TimeUnit.SECONDS).syncUninterruptibly();
+        sessionExecutor.shutdownNow();
+    }
+
+    boolean awaitTermination(long timeout, TimeUnit unit) throws 
InterruptedException {
+        long deadline = System.nanoTime() + unit.toNanos(timeout);
+        return bossGroup.terminationFuture().await(timeout, unit)
+                && workerGroup.terminationFuture().await(timeout, unit)
+                && sessionExecutor.awaitTermination(Math.max(0, deadline - 
System.nanoTime()), TimeUnit.NANOSECONDS);
+    }
+
+    private void accept(Connection connection) {
+        sessionExecutor.submit(() -> {
+            try {
+                AeshBackend backend = new AeshBackend(connection);
+                CamelMonitor monitor = new CamelMonitor(main, classLoader);
+                monitor.name = name;
+                monitor.refreshInterval = refreshInterval;
+                monitor.theme = theme;
+                monitor.webBackend = backend;
+                monitor.call();
+            } catch (Exception e) {
+                LOG.log(Level.WARNING, "Web TUI session ended with an error", 
e);
+            } finally {
+                try {
+                    connection.close();
+                } catch (Exception ignored) {
+                    // connection already closing
+                }
+            }
+        });
+    }
+
+    private final class WebServerInitializer extends 
ChannelInitializer<SocketChannel> {
+
+        @Override
+        protected void initChannel(SocketChannel channel) {
+            ChannelPipeline pipeline = channel.pipeline();
+            pipeline.addLast(new HttpServerCodec());
+            pipeline.addLast(new ChunkedWriteHandler());
+            pipeline.addLast(new HttpObjectAggregator(65_536));
+            pipeline.addLast(new OriginCheckingUpgradeHandler());
+            pipeline.addLast(new HttpRequestHandler("/ws", "/tui/web"));
+            pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));
+            pipeline.addLast(new TtyWebSocketFrameHandler(channels, 
TuiWebServer.this::accept));
+        }
+    }
+
+    private final class OriginCheckingUpgradeHandler extends 
SimpleChannelInboundHandler<FullHttpRequest> {
+
+        @Override
+        protected void channelRead0(ChannelHandlerContext context, 
FullHttpRequest request) {
+            if ("/ws".equalsIgnoreCase(request.uri()) && 
!isAllowedOrigin(request)) {

Review Comment:
   **[Medium — Grok]** Origin check uses exact 
`"/ws".equalsIgnoreCase(request.uri())`. If the upgrade URI includes a query 
string (e.g. `/ws?token=…`), this check is skipped and a foreign `Origin` may 
reach the WebSocket handler.
   
   **Suggestion:** Parse the path only (`QueryStringDecoder` or strip `?…`) 
before comparing, or use `startsWith("/ws")` with a boundary check.



##########
dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiWebServer.java:
##########
@@ -0,0 +1,198 @@
+/*
+ * 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.io.IOException;
+import java.lang.System.Logger;
+import java.lang.System.Logger.Level;
+import java.net.BindException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+import dev.tamboui.backend.aesh.AeshBackend;
+import io.netty.bootstrap.ServerBootstrap;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInitializer;
+import io.netty.channel.ChannelPipeline;
+import io.netty.channel.EventLoopGroup;
+import io.netty.channel.MultiThreadIoEventLoopGroup;
+import io.netty.channel.SimpleChannelInboundHandler;
+import io.netty.channel.group.ChannelGroup;
+import io.netty.channel.group.DefaultChannelGroup;
+import io.netty.channel.nio.NioIoHandler;
+import io.netty.channel.socket.SocketChannel;
+import io.netty.channel.socket.nio.NioServerSocketChannel;
+import io.netty.handler.codec.http.DefaultFullHttpResponse;
+import io.netty.handler.codec.http.FullHttpRequest;
+import io.netty.handler.codec.http.HttpHeaderNames;
+import io.netty.handler.codec.http.HttpObjectAggregator;
+import io.netty.handler.codec.http.HttpResponseStatus;
+import io.netty.handler.codec.http.HttpServerCodec;
+import io.netty.handler.codec.http.HttpVersion;
+import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
+import io.netty.handler.stream.ChunkedWriteHandler;
+import io.netty.util.concurrent.ImmediateEventExecutor;
+import org.aesh.terminal.Connection;
+import org.aesh.terminal.http.netty.HttpRequestHandler;
+import org.aesh.terminal.http.netty.TtyWebSocketFrameHandler;
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+
+/**
+ * Serves the Camel TUI dashboard to a web browser over WebSocket, using 
Aesh's HTTP/WebSocket terminal bridge.
+ * <p>
+ * Each incoming connection gets its own {@link CamelMonitor} instance 
(running the same live-monitoring logic as a
+ * local terminal session) driven by an {@link AeshBackend} wrapping that 
connection.
+ * <p>
+ * Binds to 127.0.0.1 only for security.
+ */
+class TuiWebServer {
+
+    private static final Logger LOG = 
System.getLogger(TuiWebServer.class.getName());
+    private final int port;
+    private final CamelJBangMain main;
+    private final ClassLoader classLoader;
+    private final String name;
+    private final long refreshInterval;
+    private final String theme;
+    private final ChannelGroup channels = new 
DefaultChannelGroup(ImmediateEventExecutor.INSTANCE);
+    private final EventLoopGroup bossGroup = new 
MultiThreadIoEventLoopGroup(1, NioIoHandler.newFactory());
+    private final EventLoopGroup workerGroup = new 
MultiThreadIoEventLoopGroup(NioIoHandler.newFactory());
+    private final ExecutorService sessionExecutor = 
Executors.newFixedThreadPool(

Review Comment:
   **[Medium — Grok]** Fixed-size pool but `Executors.newFixedThreadPool` uses 
an **unbounded** task queue. Each WebSocket accept enqueues a full 
`CamelMonitor.call()`; a localhost client opening many connections can queue 
unbounded work and exhaust memory/CPU even though only `max(4, 2×cpus)` run 
concurrently.
   
   **Suggestion:** Consider a bounded queue with a rejection policy, connection 
limit, or semaphore on admissions.



##########
dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiWebServer.java:
##########
@@ -0,0 +1,198 @@
+/*
+ * 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.io.IOException;
+import java.lang.System.Logger;
+import java.lang.System.Logger.Level;
+import java.net.BindException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+import dev.tamboui.backend.aesh.AeshBackend;
+import io.netty.bootstrap.ServerBootstrap;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInitializer;
+import io.netty.channel.ChannelPipeline;
+import io.netty.channel.EventLoopGroup;
+import io.netty.channel.MultiThreadIoEventLoopGroup;
+import io.netty.channel.SimpleChannelInboundHandler;
+import io.netty.channel.group.ChannelGroup;
+import io.netty.channel.group.DefaultChannelGroup;
+import io.netty.channel.nio.NioIoHandler;
+import io.netty.channel.socket.SocketChannel;
+import io.netty.channel.socket.nio.NioServerSocketChannel;
+import io.netty.handler.codec.http.DefaultFullHttpResponse;
+import io.netty.handler.codec.http.FullHttpRequest;
+import io.netty.handler.codec.http.HttpHeaderNames;
+import io.netty.handler.codec.http.HttpObjectAggregator;
+import io.netty.handler.codec.http.HttpResponseStatus;
+import io.netty.handler.codec.http.HttpServerCodec;
+import io.netty.handler.codec.http.HttpVersion;
+import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
+import io.netty.handler.stream.ChunkedWriteHandler;
+import io.netty.util.concurrent.ImmediateEventExecutor;
+import org.aesh.terminal.Connection;
+import org.aesh.terminal.http.netty.HttpRequestHandler;
+import org.aesh.terminal.http.netty.TtyWebSocketFrameHandler;
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+
+/**
+ * Serves the Camel TUI dashboard to a web browser over WebSocket, using 
Aesh's HTTP/WebSocket terminal bridge.
+ * <p>
+ * Each incoming connection gets its own {@link CamelMonitor} instance 
(running the same live-monitoring logic as a
+ * local terminal session) driven by an {@link AeshBackend} wrapping that 
connection.
+ * <p>
+ * Binds to 127.0.0.1 only for security.
+ */
+class TuiWebServer {
+
+    private static final Logger LOG = 
System.getLogger(TuiWebServer.class.getName());
+    private final int port;
+    private final CamelJBangMain main;
+    private final ClassLoader classLoader;
+    private final String name;
+    private final long refreshInterval;
+    private final String theme;
+    private final ChannelGroup channels = new 
DefaultChannelGroup(ImmediateEventExecutor.INSTANCE);
+    private final EventLoopGroup bossGroup = new 
MultiThreadIoEventLoopGroup(1, NioIoHandler.newFactory());
+    private final EventLoopGroup workerGroup = new 
MultiThreadIoEventLoopGroup(NioIoHandler.newFactory());
+    private final ExecutorService sessionExecutor = 
Executors.newFixedThreadPool(
+            Math.max(4, Runtime.getRuntime().availableProcessors() * 2), r -> {
+                Thread t = new Thread(r, "tui-web-session");
+                t.setDaemon(true);
+                return t;
+            });
+    private Channel serverChannel;
+    private boolean stopped;
+
+    TuiWebServer(int port, CamelJBangMain main, ClassLoader classLoader, 
String name, long refreshInterval,
+                 String theme) {
+        this.port = port;
+        this.main = main;
+        this.classLoader = classLoader;
+        this.name = name;
+        this.refreshInterval = refreshInterval;
+        this.theme = theme;
+    }
+
+    void start() throws IOException {
+        try {
+            ServerBootstrap bootstrap = new ServerBootstrap();
+            serverChannel = bootstrap.group(bossGroup, workerGroup)
+                    .channel(NioServerSocketChannel.class)
+                    .childHandler(new WebServerInitializer())
+                    .bind("127.0.0.1", port)
+                    .sync()
+                    .channel();
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            stop();
+            throw new IOException("Interrupted while starting web terminal 
server", e);
+        } catch (Exception e) {
+            stop();
+            Throwable cause = e.getCause();
+            if (cause instanceof BindException bindException) {
+                throw bindException;
+            }
+            if (e instanceof BindException bindException) {
+                throw bindException;
+            }
+            throw new IOException("Failed to start web terminal server", e);
+        }
+    }
+
+    synchronized void stop() {
+        if (stopped) {
+            return;
+        }
+        stopped = true;
+        if (serverChannel != null) {
+            serverChannel.close().syncUninterruptibly();
+        }
+        channels.close().syncUninterruptibly();
+        bossGroup.shutdownGracefully(0, 5, 
TimeUnit.SECONDS).syncUninterruptibly();
+        workerGroup.shutdownGracefully(0, 5, 
TimeUnit.SECONDS).syncUninterruptibly();
+        sessionExecutor.shutdownNow();
+    }
+
+    boolean awaitTermination(long timeout, TimeUnit unit) throws 
InterruptedException {
+        long deadline = System.nanoTime() + unit.toNanos(timeout);
+        return bossGroup.terminationFuture().await(timeout, unit)
+                && workerGroup.terminationFuture().await(timeout, unit)
+                && sessionExecutor.awaitTermination(Math.max(0, deadline - 
System.nanoTime()), TimeUnit.NANOSECONDS);
+    }
+
+    private void accept(Connection connection) {
+        sessionExecutor.submit(() -> {

Review Comment:
   **[Low — Grok]** Bare `sessionExecutor.submit(...)` with no 
`RejectedExecutionException` handling. After `stop()` calls `shutdownNow()`, a 
late WebSocket accept on a Netty thread can throw on the event loop.
   
   **Suggestion:** Catch `RejectedExecutionException` in `accept()` and close 
the connection cleanly.



##########
dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java:
##########
@@ -752,7 +790,12 @@ private boolean handleGlobalKeys(KeyEvent ke, TuiRunner 
runner) {
         boolean textEditing = probeEditing || sourceSearchActive || 
logSearchActive || spanFilterActive
                 || beanFilterActive || classpathFilterActive || 
mavenDepsFilterActive || sqlInputActive
                 || catalogFilterActive || filesBrowserTextActive;
+        // A browser session only views the shared monitor; it must not be 
able to quit the TUI

Review Comment:
   **[Medium — Grok]** Comment says browser session "views the shared monitor" 
and must not quit the process others use — but `TuiWebServer.accept()` creates 
an **independent** `CamelMonitor` per connection (L142–151). Swallowing 
`q`/Ctrl+C here prevents the browser user from closing their own session, while 
AI `/quit` (`AiSlashCommandRegistry` L90–94 → `requestExit()` → `tui::quit()`) 
still exits that session.
   
   **Suggestion:** Either allow `q`/Ctrl+C to call `runner.quit()` for 
`webBackend != null` (only that session), or also block `/quit` in web sessions 
— and fix the comment.



##########
dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiWebServer.java:
##########
@@ -0,0 +1,198 @@
+/*
+ * 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.io.IOException;
+import java.lang.System.Logger;
+import java.lang.System.Logger.Level;
+import java.net.BindException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+import dev.tamboui.backend.aesh.AeshBackend;
+import io.netty.bootstrap.ServerBootstrap;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInitializer;
+import io.netty.channel.ChannelPipeline;
+import io.netty.channel.EventLoopGroup;
+import io.netty.channel.MultiThreadIoEventLoopGroup;
+import io.netty.channel.SimpleChannelInboundHandler;
+import io.netty.channel.group.ChannelGroup;
+import io.netty.channel.group.DefaultChannelGroup;
+import io.netty.channel.nio.NioIoHandler;
+import io.netty.channel.socket.SocketChannel;
+import io.netty.channel.socket.nio.NioServerSocketChannel;
+import io.netty.handler.codec.http.DefaultFullHttpResponse;
+import io.netty.handler.codec.http.FullHttpRequest;
+import io.netty.handler.codec.http.HttpHeaderNames;
+import io.netty.handler.codec.http.HttpObjectAggregator;
+import io.netty.handler.codec.http.HttpResponseStatus;
+import io.netty.handler.codec.http.HttpServerCodec;
+import io.netty.handler.codec.http.HttpVersion;
+import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
+import io.netty.handler.stream.ChunkedWriteHandler;
+import io.netty.util.concurrent.ImmediateEventExecutor;
+import org.aesh.terminal.Connection;
+import org.aesh.terminal.http.netty.HttpRequestHandler;
+import org.aesh.terminal.http.netty.TtyWebSocketFrameHandler;
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+
+/**
+ * Serves the Camel TUI dashboard to a web browser over WebSocket, using 
Aesh's HTTP/WebSocket terminal bridge.
+ * <p>
+ * Each incoming connection gets its own {@link CamelMonitor} instance 
(running the same live-monitoring logic as a
+ * local terminal session) driven by an {@link AeshBackend} wrapping that 
connection.
+ * <p>
+ * Binds to 127.0.0.1 only for security.
+ */
+class TuiWebServer {
+
+    private static final Logger LOG = 
System.getLogger(TuiWebServer.class.getName());
+    private final int port;
+    private final CamelJBangMain main;
+    private final ClassLoader classLoader;
+    private final String name;
+    private final long refreshInterval;
+    private final String theme;
+    private final ChannelGroup channels = new 
DefaultChannelGroup(ImmediateEventExecutor.INSTANCE);
+    private final EventLoopGroup bossGroup = new 
MultiThreadIoEventLoopGroup(1, NioIoHandler.newFactory());
+    private final EventLoopGroup workerGroup = new 
MultiThreadIoEventLoopGroup(NioIoHandler.newFactory());
+    private final ExecutorService sessionExecutor = 
Executors.newFixedThreadPool(
+            Math.max(4, Runtime.getRuntime().availableProcessors() * 2), r -> {
+                Thread t = new Thread(r, "tui-web-session");
+                t.setDaemon(true);
+                return t;
+            });
+    private Channel serverChannel;
+    private boolean stopped;
+
+    TuiWebServer(int port, CamelJBangMain main, ClassLoader classLoader, 
String name, long refreshInterval,
+                 String theme) {
+        this.port = port;
+        this.main = main;
+        this.classLoader = classLoader;
+        this.name = name;
+        this.refreshInterval = refreshInterval;
+        this.theme = theme;
+    }
+
+    void start() throws IOException {
+        try {
+            ServerBootstrap bootstrap = new ServerBootstrap();
+            serverChannel = bootstrap.group(bossGroup, workerGroup)
+                    .channel(NioServerSocketChannel.class)
+                    .childHandler(new WebServerInitializer())
+                    .bind("127.0.0.1", port)
+                    .sync()
+                    .channel();
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            stop();
+            throw new IOException("Interrupted while starting web terminal 
server", e);
+        } catch (Exception e) {
+            stop();
+            Throwable cause = e.getCause();
+            if (cause instanceof BindException bindException) {
+                throw bindException;
+            }
+            if (e instanceof BindException bindException) {
+                throw bindException;
+            }
+            throw new IOException("Failed to start web terminal server", e);
+        }
+    }
+
+    synchronized void stop() {
+        if (stopped) {
+            return;
+        }
+        stopped = true;
+        if (serverChannel != null) {
+            serverChannel.close().syncUninterruptibly();
+        }
+        channels.close().syncUninterruptibly();
+        bossGroup.shutdownGracefully(0, 5, 
TimeUnit.SECONDS).syncUninterruptibly();
+        workerGroup.shutdownGracefully(0, 5, 
TimeUnit.SECONDS).syncUninterruptibly();
+        sessionExecutor.shutdownNow();
+    }
+
+    boolean awaitTermination(long timeout, TimeUnit unit) throws 
InterruptedException {

Review Comment:
   **[Low — Grok]** `awaitTermination(timeout)` waits the full `timeout` on 
`bossGroup`, then again on `workerGroup`, then only the *remaining* time on 
`sessionExecutor`. Total wait can approach **2× the requested timeout**.
   
   **Suggestion:** Track a single deadline and pass `max(0, deadline - now)` to 
each await call.



##########
dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiWebServer.java:
##########
@@ -0,0 +1,198 @@
+/*
+ * 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.io.IOException;
+import java.lang.System.Logger;
+import java.lang.System.Logger.Level;
+import java.net.BindException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+import dev.tamboui.backend.aesh.AeshBackend;
+import io.netty.bootstrap.ServerBootstrap;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInitializer;
+import io.netty.channel.ChannelPipeline;
+import io.netty.channel.EventLoopGroup;
+import io.netty.channel.MultiThreadIoEventLoopGroup;
+import io.netty.channel.SimpleChannelInboundHandler;
+import io.netty.channel.group.ChannelGroup;
+import io.netty.channel.group.DefaultChannelGroup;
+import io.netty.channel.nio.NioIoHandler;
+import io.netty.channel.socket.SocketChannel;
+import io.netty.channel.socket.nio.NioServerSocketChannel;
+import io.netty.handler.codec.http.DefaultFullHttpResponse;
+import io.netty.handler.codec.http.FullHttpRequest;
+import io.netty.handler.codec.http.HttpHeaderNames;
+import io.netty.handler.codec.http.HttpObjectAggregator;
+import io.netty.handler.codec.http.HttpResponseStatus;
+import io.netty.handler.codec.http.HttpServerCodec;
+import io.netty.handler.codec.http.HttpVersion;
+import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
+import io.netty.handler.stream.ChunkedWriteHandler;
+import io.netty.util.concurrent.ImmediateEventExecutor;
+import org.aesh.terminal.Connection;
+import org.aesh.terminal.http.netty.HttpRequestHandler;
+import org.aesh.terminal.http.netty.TtyWebSocketFrameHandler;
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+
+/**
+ * Serves the Camel TUI dashboard to a web browser over WebSocket, using 
Aesh's HTTP/WebSocket terminal bridge.
+ * <p>
+ * Each incoming connection gets its own {@link CamelMonitor} instance 
(running the same live-monitoring logic as a
+ * local terminal session) driven by an {@link AeshBackend} wrapping that 
connection.
+ * <p>
+ * Binds to 127.0.0.1 only for security.
+ */
+class TuiWebServer {
+
+    private static final Logger LOG = 
System.getLogger(TuiWebServer.class.getName());
+    private final int port;
+    private final CamelJBangMain main;
+    private final ClassLoader classLoader;
+    private final String name;
+    private final long refreshInterval;
+    private final String theme;
+    private final ChannelGroup channels = new 
DefaultChannelGroup(ImmediateEventExecutor.INSTANCE);
+    private final EventLoopGroup bossGroup = new 
MultiThreadIoEventLoopGroup(1, NioIoHandler.newFactory());
+    private final EventLoopGroup workerGroup = new 
MultiThreadIoEventLoopGroup(NioIoHandler.newFactory());
+    private final ExecutorService sessionExecutor = 
Executors.newFixedThreadPool(
+            Math.max(4, Runtime.getRuntime().availableProcessors() * 2), r -> {
+                Thread t = new Thread(r, "tui-web-session");
+                t.setDaemon(true);
+                return t;
+            });
+    private Channel serverChannel;
+    private boolean stopped;
+
+    TuiWebServer(int port, CamelJBangMain main, ClassLoader classLoader, 
String name, long refreshInterval,
+                 String theme) {
+        this.port = port;
+        this.main = main;
+        this.classLoader = classLoader;
+        this.name = name;
+        this.refreshInterval = refreshInterval;
+        this.theme = theme;
+    }
+
+    void start() throws IOException {
+        try {
+            ServerBootstrap bootstrap = new ServerBootstrap();
+            serverChannel = bootstrap.group(bossGroup, workerGroup)
+                    .channel(NioServerSocketChannel.class)
+                    .childHandler(new WebServerInitializer())
+                    .bind("127.0.0.1", port)
+                    .sync()
+                    .channel();
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            stop();
+            throw new IOException("Interrupted while starting web terminal 
server", e);
+        } catch (Exception e) {
+            stop();
+            Throwable cause = e.getCause();
+            if (cause instanceof BindException bindException) {
+                throw bindException;
+            }
+            if (e instanceof BindException bindException) {
+                throw bindException;
+            }
+            throw new IOException("Failed to start web terminal server", e);
+        }
+    }
+
+    synchronized void stop() {
+        if (stopped) {
+            return;
+        }
+        stopped = true;
+        if (serverChannel != null) {
+            serverChannel.close().syncUninterruptibly();
+        }
+        channels.close().syncUninterruptibly();
+        bossGroup.shutdownGracefully(0, 5, 
TimeUnit.SECONDS).syncUninterruptibly();
+        workerGroup.shutdownGracefully(0, 5, 
TimeUnit.SECONDS).syncUninterruptibly();
+        sessionExecutor.shutdownNow();
+    }
+
+    boolean awaitTermination(long timeout, TimeUnit unit) throws 
InterruptedException {
+        long deadline = System.nanoTime() + unit.toNanos(timeout);
+        return bossGroup.terminationFuture().await(timeout, unit)
+                && workerGroup.terminationFuture().await(timeout, unit)
+                && sessionExecutor.awaitTermination(Math.max(0, deadline - 
System.nanoTime()), TimeUnit.NANOSECONDS);
+    }
+
+    private void accept(Connection connection) {
+        sessionExecutor.submit(() -> {
+            try {
+                AeshBackend backend = new AeshBackend(connection);
+                CamelMonitor monitor = new CamelMonitor(main, classLoader);
+                monitor.name = name;
+                monitor.refreshInterval = refreshInterval;
+                monitor.theme = theme;
+                monitor.webBackend = backend;
+                monitor.call();
+            } catch (Exception e) {
+                LOG.log(Level.WARNING, "Web TUI session ended with an error", 
e);
+            } finally {
+                try {
+                    connection.close();
+                } catch (Exception ignored) {
+                    // connection already closing
+                }
+            }
+        });
+    }
+
+    private final class WebServerInitializer extends 
ChannelInitializer<SocketChannel> {
+
+        @Override
+        protected void initChannel(SocketChannel channel) {
+            ChannelPipeline pipeline = channel.pipeline();
+            pipeline.addLast(new HttpServerCodec());
+            pipeline.addLast(new ChunkedWriteHandler());
+            pipeline.addLast(new HttpObjectAggregator(65_536));
+            pipeline.addLast(new OriginCheckingUpgradeHandler());
+            pipeline.addLast(new HttpRequestHandler("/ws", "/tui/web"));

Review Comment:
   **[Medium — Grok / security]** Static page is served without 
`X-Frame-Options: DENY` or `Content-Security-Policy: frame-ancestors 'none'`. A 
remote site can iframe `http://127.0.0.1:<port>/`; the browser then sends a 
loopback `Origin` on WebSocket upgrade (allowed), enabling **clickjacking** of 
the full TUI on the loopback trust boundary.
   
   **Suggestion:** Add frame-denial headers in `HttpRequestHandler` or a small 
Netty handler for all HTML responses.



##########
docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc:
##########
@@ -760,6 +760,25 @@ The MCP server exposes tools organized by purpose:
 
 See xref:camel-jbang-mcp.adoc[Camel MCP Server] for more about MCP and AI 
integration with Camel.
 
+== Web Browser Access
+
+The TUI can also be reached from a web browser on the same host, using the 
same dashboard you'd see in a
+local terminal -- useful when you prefer a browser session to a terminal 
window.
+
+[source,bash]
+----
+camel tui --web
+----
+
+This starts a web terminal server on `localhost:8090` (configurable with 
`--web-port`).
+Open `http://localhost:8090` in a browser to get a full xterm.js terminal 
driving the same
+TUI dashboard, with the same tabs, keyboard shortcuts, and F2 actions menu as 
a local session.
+
+Like the MCP server, the web server is bound to `127.0.0.1` only -- it never 
listens on
+external interfaces -- and there is no authentication beyond that. Each 
browser connection
+gets its own independent TUI session (its own process discovery and navigation 
state), the

Review Comment:
   **[Low — docs]** States browser sessions have "the same … keyboard 
shortcuts" as local terminal, but web sessions hide the `q`/quit footer hint 
(`OverviewTab`) and swallow `q`/Ctrl+C (`CamelMonitor` L795–798). Consider 
noting that quit behaves differently in browser sessions.



##########
dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiWebServerTest.java:
##########
@@ -0,0 +1,213 @@
+/*
+ * 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.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.net.BindException;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.net.http.WebSocket;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.apache.camel.test.AvailablePortFinder;
+import org.apache.camel.test.AvailablePortFinder.Port;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.parallel.Isolated;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+@Isolated
+class TuiWebServerTest {
+
+    private TuiWebServer server;
+
+    @AfterEach
+    void tearDown() {
+        if (server != null) {
+            server.stop();
+        }
+    }
+
+    @Test
+    void startBindsToLoopbackAndAcceptsTcpConnections() throws Exception {
+        try (Port reserved = AvailablePortFinder.find()) {
+            server = newServer(reserved.getPort());
+
+            server.start();
+
+            try (Socket socket = new Socket()) {
+                socket.connect(new InetSocketAddress("127.0.0.1", 
reserved.getPort()), 2000);
+                assertThat(socket.isConnected()).isTrue();
+            }
+        }
+    }
+
+    @Test
+    void stopClosesTheListeningPort() throws Exception {
+        try (Port reserved = AvailablePortFinder.find()) {
+            server = newServer(reserved.getPort());
+            server.start();
+
+            server.stop();
+
+            assertThatThrownBy(() -> {
+                try (Socket socket = new Socket()) {
+                    socket.connect(new InetSocketAddress("127.0.0.1", 
reserved.getPort()), 2000);
+                }
+            }).isInstanceOf(IOException.class);
+        }
+    }
+
+    @Test
+    void rejectsWebSocketUpgradeFromForeignOrigin() throws Exception {
+        try (Port reserved = AvailablePortFinder.find()) {
+            server = newServer(reserved.getPort());
+            server.start();
+
+            assertThat(webSocketHandshake(reserved.getPort(), 
"https://attacker.invalid";))
+                    .startsWith("HTTP/1.1 403");
+        }
+    }
+
+    @Test
+    void acceptsWebSocketUpgradeFromTheLoopbackPage() throws Exception {
+        try (Port reserved = AvailablePortFinder.find()) {
+            server = newServer(reserved.getPort());
+            server.start();
+
+            assertThat(webSocketHandshake(reserved.getPort(), 
"http://127.0.0.1:"; + reserved.getPort()))

Review Comment:
   **[Medium — Grok]** A successful `101` handshake triggers 
`TtyWebSocketFrameHandler` → `accept()` → full `CamelMonitor.call()` on a 
background thread. This test only asserts the status line but may spawn a 
heavyweight monitor session with no guaranteed teardown before `@AfterEach 
stop()`.
   
   **Suggestion:** Mock/stub the accept callback in unit tests, or assert 
session cleanup; consider `@Isolated` + short-lived connection abort (as in 
L146) immediately after handshake.



##########
dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiBackendHelper.java:
##########
@@ -29,10 +30,15 @@ private TuiBackendHelper() {
 
     static TuiRunner createTuiRunner() throws Exception {
         Terminal activeTerminal = EnvironmentHelper.getActiveTerminal();
-        if (activeTerminal != null) {
-            JLineBackend backend = new JLineBackend(activeTerminal);
-            return 
TuiRunner.create(TuiConfig.builder().backend(backend).mouseCapture(true).build());
-        }
-        return 
TuiRunner.create(TuiConfig.builder().mouseCapture(true).build());
+        // Build the JLine backend explicitly rather than leaving backend 
selection to
+        // TamboUI's ServiceLoader-based auto-discovery: with 
tamboui-aesh-backend also on the
+        // classpath (for --web), auto-discovery can pick AeshBackend for the 
local session too,
+        // which drives a native PosixSysTerminal that doesn't shut down 
cleanly here.

Review Comment:
   **[Info — looks good]** Explicit JLine backend avoids ServiceLoader picking 
`AeshBackend` for the local session when `tamboui-aesh-backend` is on the 
classpath. Good fix for clean shutdown.



##########
dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/web/index.html:
##########
@@ -0,0 +1,277 @@
+<!--
+
+    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.
+
+-->
+<!doctype html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>Camel TUI</title>
+  <link rel="stylesheet" href="/vendor/xterm.css" />
+  <style>
+    :root {
+      --camel-orange: #e97826;
+      --camel-navy:   #303284;
+      --camel-white:  #fff;
+    }
+    * { box-sizing: border-box; }
+    html, body {
+      margin: 0;
+      padding: 0;
+      height: 100vh;
+      overflow: hidden;
+      background: #1e1e1e;
+      font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", 
Roboto, sans-serif;
+    }
+    body {
+      display: flex;
+      flex-direction: column;
+    }
+
+    .site-nav {
+      flex-shrink: 0;
+      display: flex;
+      align-items: center;
+      gap: .75rem;
+      height: 52px;
+      padding: 0 1.25rem;
+      background: var(--camel-navy);
+      color: var(--camel-white);
+      border-bottom: 3px solid var(--camel-orange);
+      box-shadow: 0 2px 10px rgba(0, 0, 0, .35);
+    }
+    .logo-mark {
+      flex-shrink: 0;
+      width: 28px;
+      height: 28px;
+      object-fit: cover;
+      object-position: left center;
+    }
+    .brand-name { font-weight: 700; font-size: .92rem; }
+    .brand-sub  { font-size: .68rem; opacity: .65; }
+    .nav-spacer { flex: 1; }
+
+    .connection-status {
+      flex-shrink: 0;
+      padding: 3px 10px;
+      border-radius: 99px;
+      font-size: .68rem;
+      font-weight: 700;
+      letter-spacing: .04em;
+      text-transform: uppercase;
+      white-space: nowrap;
+    }
+    .status-connecting   { background: #4a3a1a; color: #fbbf24; }
+    .status-connected    { background: #1a472a; color: #4ade80; }
+    .status-disconnected { background: #4a1a1a; color: #f87171; }
+
+    #terminal-wrapper {
+      position: relative;
+      flex: 1;
+      min-height: 0;
+    }
+    #terminal-container {
+      position: absolute;
+      inset: 0;
+      padding: 12px;
+      background: #000;
+    }
+
+    .disconnect-overlay {
+      position: absolute;
+      inset: 0;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      background: rgba(0, 0, 0, .82);
+    }
+    .disconnect-overlay[hidden] { display: none; }
+    .disconnect-box {
+      max-width: 26rem;
+      padding: 1.25rem 1.5rem;
+      border-radius: 8px;
+      border-left: 4px solid var(--camel-orange);
+      background: #232323;
+      color: var(--camel-white);
+      box-shadow: 0 10px 30px rgba(0, 0, 0, .5);
+    }
+    .disconnect-title {
+      font-weight: 700;
+      font-size: 1rem;
+      margin-bottom: .35rem;
+    }
+    .disconnect-detail {
+      font-size: .85rem;
+      opacity: .8;
+    }
+  </style>
+</head>
+<body>
+  <nav class="site-nav" aria-label="Site navigation">
+    <img class="logo-mark" src="/images/camel-logo.png" alt="" 
aria-hidden="true">
+    <div>
+      <div class="brand-name">Apache Camel</div>
+      <div class="brand-sub">Camel TUI</div>
+    </div>
+    <div class="nav-spacer"></div>
+    <div id="status" class="connection-status 
status-connecting">Connecting...</div>
+  </nav>
+  <div id="terminal-wrapper">
+    <div id="terminal-container"></div>
+    <div id="disconnect-overlay" class="disconnect-overlay" hidden>
+      <div class="disconnect-box">
+        <div class="disconnect-title">Connection lost</div>
+        <div class="disconnect-detail" id="disconnect-detail"></div>
+      </div>
+    </div>
+  </div>
+
+  <script src="/vendor/xterm.js"></script>
+  <script src="/vendor/xterm-addon-fit.js"></script>
+  <script>
+    (function () {
+      'use strict';
+
+      var statusEl = document.getElementById('status');
+      var overlayEl = document.getElementById('disconnect-overlay');
+      var overlayDetailEl = document.getElementById('disconnect-detail');
+
+      function setStatus(status, message) {
+        statusEl.className = 'connection-status status-' + status;
+        statusEl.textContent = message;
+      }
+
+      function showDisconnectOverlay(message) {
+        overlayDetailEl.textContent = message;
+        overlayEl.hidden = false;
+      }
+
+      function hideDisconnectOverlay() {
+        overlayEl.hidden = true;
+      }
+
+      function connect() {
+        var wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 
'ws:';
+        var wsUrl = wsProtocol + '//' + window.location.host + '/ws';
+
+        setStatus('connecting', 'Connecting...');
+        hideDisconnectOverlay();
+
+        var socket = new WebSocket(wsUrl);
+        var term = null;
+        var fitAddon = null;
+        var resizeTimeout = null;
+
+        socket.onopen = function () {
+          setStatus('connected', 'Connected');
+
+          term = new Terminal({
+            cursorBlink: true,
+            cursorStyle: 'block',
+            fontFamily: '"DejaVu Sans Mono", "Liberation Mono", "Courier New", 
monospace',
+            fontSize: 14,
+            theme: {
+              background: '#000000',
+              foreground: '#f0f0f0',
+              cursor: '#f0f0f0',
+              cursorAccent: '#000000',
+              selectionBackground: 'rgba(255, 255, 255, 0.3)'
+            },
+            allowProposedApi: true
+          });
+
+          fitAddon = new FitAddon.FitAddon();
+          term.loadAddon(fitAddon);
+          term.open(document.getElementById('terminal-container'));
+
+          function sendInit() {
+            fitAddon.fit();
+            // Message shape ({action: 'init'|'read'|'resize', ...}) is 
dictated by
+            // org.aesh:terminal-http's server-side HttpTtyConnection - it is 
not ours to change.
+            socket.send(JSON.stringify({
+              action: 'init',
+              type: 'xterm-256color',
+              cols: term.cols,
+              rows: term.rows,
+              userAgent: navigator.userAgent
+            }));
+          }
+
+          // fitAddon.fit() right after open() can measure a stale/default 
size before the
+          // browser has finished laying out the page - defer past one paint 
so the server
+          // starts with the terminal's real dimensions instead of drifting 
and needing a
+          // resize correction.
+          requestAnimationFrame(function () {
+            requestAnimationFrame(sendInit);
+          });
+
+          socket.onmessage = function (event) {
+            term.write(event.data);
+          };
+
+          term.onData(function (data) {
+            socket.send(JSON.stringify({ action: 'read', data: data }));
+          });
+
+          term.onResize(function (size) {
+            socket.send(JSON.stringify({ action: 'resize', cols: size.cols, 
rows: size.rows }));
+          });
+
+          function handleResize() {
+            if (resizeTimeout) {
+              clearTimeout(resizeTimeout);
+            }
+            resizeTimeout = setTimeout(function () {
+              if (fitAddon && term) {
+                fitAddon.fit();
+              }
+            }, 100);
+          }
+
+          window.addEventListener('resize', handleResize);
+          if (window.ResizeObserver) {

Review Comment:
   **[Low — Grok]** On disconnect, `window.removeEventListener('resize', …)` 
runs but the `ResizeObserver` is never disconnected and the `Terminal` instance 
is never disposed. Reloading is required to GC; long-lived tabs that 
disconnect/reconnect would leak listeners.
   
   **Suggestion:** Store observer reference and call `disconnect()` in 
`onclose`; call `term.dispose()` if available.



##########
dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiWebServerTest.java:
##########
@@ -0,0 +1,213 @@
+/*
+ * 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.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.net.BindException;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.net.http.WebSocket;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.apache.camel.test.AvailablePortFinder;
+import org.apache.camel.test.AvailablePortFinder.Port;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.parallel.Isolated;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+@Isolated
+class TuiWebServerTest {
+
+    private TuiWebServer server;
+
+    @AfterEach
+    void tearDown() {
+        if (server != null) {
+            server.stop();
+        }
+    }
+
+    @Test
+    void startBindsToLoopbackAndAcceptsTcpConnections() throws Exception {
+        try (Port reserved = AvailablePortFinder.find()) {
+            server = newServer(reserved.getPort());
+
+            server.start();
+
+            try (Socket socket = new Socket()) {
+                socket.connect(new InetSocketAddress("127.0.0.1", 
reserved.getPort()), 2000);
+                assertThat(socket.isConnected()).isTrue();
+            }
+        }
+    }
+
+    @Test
+    void stopClosesTheListeningPort() throws Exception {
+        try (Port reserved = AvailablePortFinder.find()) {
+            server = newServer(reserved.getPort());
+            server.start();
+
+            server.stop();
+
+            assertThatThrownBy(() -> {
+                try (Socket socket = new Socket()) {
+                    socket.connect(new InetSocketAddress("127.0.0.1", 
reserved.getPort()), 2000);
+                }
+            }).isInstanceOf(IOException.class);
+        }
+    }
+
+    @Test
+    void rejectsWebSocketUpgradeFromForeignOrigin() throws Exception {
+        try (Port reserved = AvailablePortFinder.find()) {
+            server = newServer(reserved.getPort());
+            server.start();
+
+            assertThat(webSocketHandshake(reserved.getPort(), 
"https://attacker.invalid";))
+                    .startsWith("HTTP/1.1 403");
+        }
+    }
+
+    @Test

Review Comment:
   **[Low — coverage gap]** Tests cover `127.0.0.1` origin and foreign origin, 
but not `http://localhost:<port>` (also allowed by `isAllowedOrigin`). Worth 
adding for symmetry.



##########
dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java:
##########
@@ -539,9 +569,14 @@ public void resetIntegrationTabState() {
             applyLogPin();
             applyRatePer();
             applyConfirmActions();
-            // Intercept Ctrl+C: quit the TUI cleanly instead of letting
-            // the JVM tear down the classloader while we're still running
-            Signal.handle(new Signal("INT"), sig -> tui.quit());
+            if (webBackend == null) {
+                // Intercept Ctrl+C: quit the TUI cleanly instead of letting
+                // the JVM tear down the classloader while we're still running.
+                // Signal.handle is process-wide and would clobber concurrent 
sessions
+                // (e.g. browser connections via --web), so only the local 
terminal
+                // session registers it.
+                Signal.handle(new Signal("INT"), sig -> tui.quit());

Review Comment:
   **[Info — looks good]** Correctly scopes `Signal.handle(INT)` to the local 
terminal session only. Process-wide signal handlers would break concurrent 
browser sessions.



##########
docs/user-manual/modules/ROOT/pages/security-model.adoc:
##########
@@ -641,6 +641,17 @@ be closed as `not a vulnerability`.
   "MBean operation X executes code or sends to endpoint Y when invoked
   from a JMX or Jolokia connection" describes the documented contract,
   not a framework vulnerability.
+* *The Camel TUI's `--mcp` and `--web` servers.* Camel TUI

Review Comment:
   **[Info — looks good]** Clear framing of `--web` alongside `--mcp` as opt-in 
loopback management surfaces. Matches Camel's documented trust model.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to