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)
Sidebar Layout and Mobile Optimization
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.