Documents
Home>Documents>AI>Agent>Geny

Building a Web UI Dashboard for Claude Control

6 min readFeb 5, 2026Feb 22, 2026

Claude Control Web UI Dashboard Development

Managing sessions through the terminal alone had clear limitations. To view the status of multiple sessions at a glance, send prompts, and check logs, a web dashboard was necessary. On February 5th, I started building the UI in earnest.

Dashboard Layout Design

The dashboard had three core components: session list, session detail, and real-time logs.

from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates

app = FastAPI()
templates = Jinja2Templates(directory="templates")

@app.get("/dashboard")
async def dashboard(request: Request):
    sessions = await session_store.get_all_sessions()
    return templates.TemplateResponse("dashboard.html", {
        "request": request,
        "sessions": sessions,
    })

WebSocket Real-Time Log Streaming

I introduced WebSockets to stream session output to the dashboard in real time.

class ConnectionManager:
    def __init__(self):
        self.active_connections: Dict[str, List[WebSocket]] = {}

    async def connect(self, session_id: str, websocket: WebSocket):
        await websocket.accept()
        if session_id not in self.active_connections:
            self.active_connections[session_id] = []
        self.active_connections[session_id].append(websocket)

    async def broadcast(self, session_id: str, message: dict):
        connections = self.active_connections.get(session_id, [])
        for connection in connections:
            try:
                await connection.send_json(message)
            except WebSocketDisconnect:
                connections.remove(connection)

ws_manager = ConnectionManager()

@app.websocket("/ws/session/{session_id}")
async def websocket_endpoint(websocket: WebSocket, session_id: str):
    await ws_manager.connect(session_id, websocket)
    try:
        while True:
            data = await websocket.receive_text()
            await handle_ws_message(session_id, data)
    except WebSocketDisconnect:
        ws_manager.disconnect(session_id, websocket)

I structured the UI with the session list in a sidebar and the selected session's details in the main area. I applied a collapsible layout to make it usable on mobile as well.

<div class="layout">
  <aside class="sidebar" :class="{ collapsed: isSidebarCollapsed }">
    <div class="session-list">
      <div v-for="session in sessions" class="session-item"
           :class="{ active: session.id === activeSessionId }"
           @click="selectSession(session.id)">
        <span class="status-dot" :style="{ background: statusColor(session.status) }"></span>
        {{ session.id }}
      </div>
    </div>
  </aside>
  <main class="content">
    <SessionDetail :session="activeSession" />
    <LogViewer :session-id="activeSessionId" />
  </main>
</div>

Lessons Learned

The hardest part of building the web dashboard was keeping real-time data in sync. Events received from Redis Pub/Sub were occasionally dropped before reaching the WebSocket pipeline, and I resolved this with reconnection logic and buffering. Having the dashboard in place multiplied the usefulness of Claude Control several times over.

Tags
DashboardWebSocketUIClaudeReal-time