Skip to content

Commit 03ebb2a

Browse files
committed
feat: Refactor UI layout and simplify SSE notifications
- Replace 'Add Server' modal with an inline form on the main page. - Rearrange page titles for better flow ('Add New MCP Server' -> 'Connected MCP Servers'). - Remove footer and unused modal templates. - Introduce `ServerDataUpdatedEvent` for backend changes. - Simplify SSE handler to listen only to `ServerDataUpdatedEvent` and broadcast a generic `refresh-list` message. - Update API handlers (`AddMCPServer`, `RemoveMCPServer`) to return HTMX feedback fragments instead of redirects. - Correct `RemoveMCPServer` route to use DELETE method.
1 parent de228ed commit 03ebb2a

File tree

17 files changed

+322
-429
lines changed

17 files changed

+322
-429
lines changed

internal/app/app.go

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ var MCPClientModule = fx.Module("mcp_client",
118118
// WebModule provides the HTTP server, API handlers, UI handlers, and SSE handler.
119119
var WebModule = fx.Module("web",
120120
fx.Provide(
121-
// Provide UI handler (depends on Logger, BackendService)
121+
// Provide UI handler struct (depends on Logger, BackendService)
122122
handlers.NewUIHandler,
123123
// Provide API handlers (depends on BackendService, Logger)
124124
func(bs backend.Service, logger log.Logger) *handlers.APIHandlers {
@@ -151,15 +151,24 @@ var WebModule = fx.Module("web",
151151
web.NewServer,
152152
),
153153
// Register handlers with the router
154-
fx.Invoke(func(mux *http.ServeMux, apiHandlers *handlers.APIHandlers, uiHandler http.Handler, sseHandler *handlers.SSEHandler) {
154+
fx.Invoke(func(mux *http.ServeMux, apiHandlers *handlers.APIHandlers, uiHandler *handlers.UIHandler, sseHandler *handlers.SSEHandler) {
155155
// API Routes (includes Swagger/OpenAPI docs)
156156
apiHandlers.RegisterRoutes(mux)
157-
// UI Route
158-
mux.Handle("/ui/", http.StripPrefix("/ui/", uiHandler)) // Serve UI under /ui/
159-
mux.Handle("/", http.RedirectHandler("/ui/", http.StatusMovedPermanently)) // Redirect root to UI
160-
// SSE Route
157+
158+
// --- UI Routes (Registered directly) ---
159+
mux.HandleFunc("/ui/", uiHandler.ServeIndex) // Note trailing slash
160+
// mux.HandleFunc("/ui/add", uiHandler.ServeAddPage) // Removed
161+
// mux.HandleFunc("/ui/success", uiHandler.ServeSuccessPage) // Removed
162+
// mux.HandleFunc("/ui/error", uiHandler.ServeErrorPage) // Removed
163+
164+
// --- Root Redirect ---
165+
// Use mux.Handle for http.RedirectHandler as it returns an http.Handler
166+
mux.Handle("/", http.RedirectHandler("/ui/", http.StatusMovedPermanently))
167+
168+
// --- SSE Route ---
161169
mux.HandleFunc("/api/events", sseHandler.ServeHTTP)
162-
// Metrics Route (already in api.go RegisterRoutes? If not, add here)
170+
171+
// Metrics Route (example, if needed)
163172
// mux.Handle("/metrics", promhttp.Handler())
164173
}),
165174
// Register web server lifecycle hooks for starting/stopping the server

internal/backend/service.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ func (s *service) AddMCPServer(name, url string) (*models.MCPServer, error) {
8181
s.logger.Debug().Interface("server", newServer).Msg("Fetched new server for event")
8282
}
8383
s.bus.Publish(events.NewServerAddedEvent(*newServer))
84+
// Publish data update event for UI
85+
s.bus.Publish(events.NewServerDataUpdatedEvent())
8486
return newServer, nil
8587
}
8688

@@ -103,6 +105,8 @@ func (s *service) RemoveMCPServer(id int64) error {
103105
}
104106
s.logger.Info().Str("name", server.Name).Str("url", serverURL).Int64("id", id).Msg("Removed MCP Server")
105107
s.bus.Publish(events.NewServerRemovedEvent(id, serverURL))
108+
// Publish data update event for UI
109+
s.bus.Publish(events.NewServerDataUpdatedEvent())
106110
return nil
107111
}
108112

@@ -183,6 +187,9 @@ func (s *service) UpdateMCPServerStatus(id int64, state models.ConnectionState,
183187
// s.bus.Publish(events.NewServerStatusChangedEvent(id, state, errStr))
184188
s.logger.Debug().Int64("id", id).Str("state", string(state)).Msg("Updated server status in DB (event published by ConnectionManager)")
185189

190+
// Publish data update event for UI AFTER DB update
191+
s.bus.Publish(events.NewServerDataUpdatedEvent())
192+
186193
// Fetching the updated server is no longer strictly necessary just for publishing the event
187194
// updatedServer, getErr := s.db.GetServerByID(id)
188195
// if getErr != nil || updatedServer == nil {
@@ -226,6 +233,10 @@ func (s *service) UpdateMCPServer(id int64, name, url string) (*models.MCPServer
226233

227234
s.logger.Info().Int64("id", id).Str("newName", name).Str("newURL", url).Msg("Updated MCP Server Details")
228235
// s.bus.Publish(events.NewServerUpdatedEvent(*updatedServer)) // TODO: Define and use a ServerUpdatedEvent
236+
237+
// Publish data update event for UI
238+
s.bus.Publish(events.NewServerDataUpdatedEvent())
239+
229240
return updatedServer, nil
230241
}
231242

@@ -338,8 +349,8 @@ func (s *service) HandleToolsUpdated(event events.Event) {
338349
s.logger.Warn().Int64("serverID", serverID).Msg("Skipping ToolsProcessedInDBEvent due to errors during tool upsert")
339350
}
340351

341-
// TODO: Potentially publish another event like 'ToolsProcessed' if other components
342-
// need to know the DB update is complete (e.g., to trigger MCP server tool refresh).
352+
// Publish data update event for UI as tool list might have changed
353+
s.bus.Publish(events.NewServerDataUpdatedEvent())
343354
}
344355

345356
// RegisterEventHandlers registers the necessary event handlers for the backend service.

internal/events/events.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ const (
2424
ToolsProcessedInDB EventType = "tools.processed.db"
2525
// LocalToolsRefreshed indicates the agent's own exposed toolset was potentially updated.
2626
LocalToolsRefreshed EventType = "local.tools.refreshed"
27+
// ServerDataUpdated signals that backend data relevant to the UI has changed.
28+
ServerDataUpdated EventType = "server.data.updated"
2729
// Add more event types as needed
2830
)
2931

@@ -141,6 +143,22 @@ func NewToolsProcessedInDBEvent(serverID int64, serverURL string) *ToolsProcesse
141143
}
142144
}
143145

146+
// --- NEW Event for UI Updates ---
147+
148+
// ServerDataUpdatedEvent is published by the BackendService after any operation
149+
// that changes data relevant to the UI (add, remove, status update, etc.).
150+
type ServerDataUpdatedEvent struct {
151+
baseEvent
152+
// Add specific change details if needed later (e.g., ServerID, ChangeType)
153+
}
154+
155+
// NewServerDataUpdatedEvent creates a new ServerDataUpdatedEvent.
156+
func NewServerDataUpdatedEvent() *ServerDataUpdatedEvent {
157+
return &ServerDataUpdatedEvent{
158+
baseEvent: newBaseEvent(ServerDataUpdated),
159+
}
160+
}
161+
144162
// LocalToolsRefreshedEvent is published by the ConnectionManager after refreshMCPServerTools completes.
145163
// It signals that the list of tools exposed by this agent's MCP server may have changed.
146164
type LocalToolsRefreshedEvent struct {

internal/mcp/server.go

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -956,11 +956,18 @@ func (cm *ConnectionManager) handleServerRemoved(event events.Event) {
956956
if _, toolsExist := cm.toolsByServer[removedEvent.ServerURL]; toolsExist {
957957
delete(cm.toolsByServer, removedEvent.ServerURL)
958958
mcpToolsTotal.WithLabelValues(removedEvent.ServerURL).Set(0)
959-
// Refreshing the MCP server tools might need to happen AFTER the backend confirms DB update.
960-
// Let's assume ToolsProcessedInDBEvent handles this refresh.
961-
// cm.refreshMCPServerTools() // Avoid calling this directly here
959+
// Refreshing the MCP server tools MUST happen after removal
960+
cm.logger.Info().Msg("Triggering local MCP tool refresh after server removal.")
961+
cm.refreshMCPServerTools()
962+
cm.mu.Unlock() // Unlock before publishing event
963+
964+
// Publish event to signal local tools changed
965+
cm.logger.Info().Msg("Publishing LocalToolsRefreshedEvent after server removal.")
966+
cm.eventBus.Publish(events.NewLocalToolsRefreshedEvent())
967+
968+
} else {
969+
cm.mu.Unlock() // Ensure unlock even if tools didn't exist
962970
}
963-
cm.mu.Unlock()
964971

965972
} else {
966973
cm.logger.Info().Str("url", removedEvent.ServerURL).Msg("No active connection found for removed server")

internal/web/handlers/api.go

Lines changed: 43 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"github.com/co-browser/agent-browser/internal/backend/models"
1313
"github.com/co-browser/agent-browser/internal/log"
1414
"github.com/co-browser/agent-browser/internal/web/templates"
15+
"github.com/co-browser/agent-browser/internal/web/templates/components"
1516
// "github.com/gorilla/mux" // Example: Assuming a router like gorilla/mux for path params
1617
)
1718

@@ -54,7 +55,7 @@ func (h *APIHandlers) RegisterRoutes(mux *http.ServeMux /* or other router type
5455
mux.HandleFunc("POST /api/mcp/servers/update/", h.routeServerUpdateRequests)
5556

5657
// For delete requests (remove server)
57-
mux.HandleFunc("POST /api/mcp/servers/remove/", h.routeServerDeleteRequests)
58+
mux.HandleFunc("DELETE /api/mcp/servers/", h.routeServerDeleteRequests)
5859

5960
// For POST requests (rediscover tools)
6061
mux.HandleFunc("POST /api/mcp/servers/", h.routeServerPostRequests)
@@ -142,15 +143,16 @@ func (h *APIHandlers) routeServerUpdateRequests(w http.ResponseWriter, r *http.R
142143

143144
// routeServerDeleteRequests handles DELETE requests for server-specific paths (Recommended structure)
144145
func (h *APIHandlers) routeServerDeleteRequests(w http.ResponseWriter, r *http.Request) {
145-
serverID, subpath, err := parseServerPath(r.URL.Path, "/api/mcp/servers/remove/")
146+
// Update prefix to match the new route pattern
147+
serverID, subpath, err := parseServerPath(r.URL.Path, "/api/mcp/servers/")
146148
if err != nil {
147149
http.Error(w, err.Error(), http.StatusBadRequest)
148150
return
149151
}
150152

151153
switch subpath {
152154
case "":
153-
// POST /api/mcp/servers/remove/:id - Remove server
155+
// DELETE /api/mcp/servers/:id - Remove server
154156
h.RemoveMCPServer(w, r, serverID)
155157
default:
156158
http.NotFound(w, r)
@@ -191,32 +193,44 @@ func (h *APIHandlers) AddMCPServer(w http.ResponseWriter, r *http.Request) {
191193
// Parse form data
192194
if err := r.ParseForm(); err != nil {
193195
h.logger.Error().Err(err).Msg("Failed to parse form data")
194-
http.Redirect(w, r, "/error?error=Form parse error", http.StatusSeeOther)
196+
w.WriteHeader(http.StatusBadRequest)
197+
// Use components package directly
198+
components.AddServerErrorFeedback("Form parse error").Render(r.Context(), w)
195199
return
196200
}
197201

198202
name := r.FormValue("name")
199203
url := r.FormValue("url")
200204

201205
if name == "" || url == "" {
202-
http.Redirect(w, r, "/error?error=Missing name or url", http.StatusSeeOther)
206+
w.WriteHeader(http.StatusBadRequest)
207+
// Use components package directly
208+
components.AddServerErrorFeedback("Missing name or url").Render(r.Context(), w)
203209
return
204210
}
205211

206-
_, err := h.backendService.AddMCPServer(name, url)
212+
server, err := h.backendService.AddMCPServer(name, url)
207213
if err != nil {
208214
h.logger.Error().Err(err).Msg("Error adding MCP server via API")
209-
210-
if err.Error() == fmt.Sprintf("MCP server with URL '%s' already exists", url) {
211-
http.Redirect(w, r, "/error?error=Server already exists", http.StatusSeeOther)
212-
return
215+
errorMsg := "Internal server error adding server"
216+
statusCode := http.StatusInternalServerError
217+
if strings.Contains(err.Error(), "already exists") {
218+
statusCode = http.StatusBadRequest
219+
errorMsg = fmt.Sprintf("Server with URL '%s' already exists", url)
213220
}
214-
215-
http.Redirect(w, r, "/error?error=Internal server error", http.StatusSeeOther)
221+
w.WriteHeader(statusCode)
222+
// Use components package directly
223+
components.AddServerErrorFeedback(errorMsg).Render(r.Context(), w)
216224
return
217225
}
218226

219-
http.Redirect(w, r, "/success", http.StatusSeeOther)
227+
// Success: Return success alert fragment for the feedback div
228+
w.WriteHeader(http.StatusOK)
229+
// Send HX-Trigger to reset form after successful request (Optional - requires JS listener)
230+
// w.Header().Set("HX-Trigger", "resetAddForm")
231+
// Use components package directly
232+
components.AddServerSuccessFeedback(server.Name).Render(r.Context(), w)
233+
// SSE refresh will handle list update separately
220234
}
221235

222236
// ListMCPServers handles GET /api/mcp/servers - Returns full JSON
@@ -239,24 +253,16 @@ func (h *APIHandlers) ListMCPServersFragment(w http.ResponseWriter, r *http.Requ
239253
servers, err := h.backendService.ListMCPServers()
240254
if err != nil {
241255
h.logger.Error().Err(err).Msg("Error listing MCP servers for fragment")
242-
// Optionally render an error message within the fragment
243-
// w.WriteHeader(http.StatusInternalServerError)
244-
// templates.ServerErrorFragment("Failed to load servers").Render(r.Context(), w)
245-
// For now, return empty list on error
246-
servers = []models.MCPServer{}
256+
w.WriteHeader(http.StatusInternalServerError)
257+
// Use components package directly for error feedback
258+
components.AddServerErrorFeedback("Failed to load server list").Render(r.Context(), w)
259+
return
247260
}
248261

249262
// Set content type to HTML
250263
w.Header().Set("Content-Type", "text/html; charset=utf-8")
251-
252-
// Render only the body component
253-
component := templates.ServerListBodyComponent(servers)
254-
err = component.Render(r.Context(), w)
255-
if err != nil {
256-
h.logger.Error().Err(err).Msg("Error rendering server list fragment")
257-
// Don't write header again if Render already wrote partial content
258-
// http.Error(w, "Failed to render server list fragment", http.StatusInternalServerError)
259-
}
264+
// Render the list body component directly using parent templates package
265+
templates.ServerListBodyComponent(servers).Render(r.Context(), w)
260266
}
261267

262268
// GetMCPServer handles GET /api/mcp/servers/:id
@@ -337,22 +343,25 @@ func (h *APIHandlers) UpdateMCPServer(w http.ResponseWriter, r *http.Request, se
337343
}
338344
}
339345

340-
// RemoveMCPServer handles POST /api/mcp/servers/:id
346+
// RemoveMCPServer handles DELETE /api/mcp/servers/:id
341347
func (h *APIHandlers) RemoveMCPServer(w http.ResponseWriter, r *http.Request, serverID int64) {
342348
err := h.backendService.RemoveMCPServer(serverID)
343-
344349
if err != nil {
345-
h.logger.Error().Err(err).Int64("serverId", serverID).Msg("Error removing MCP server via API")
346-
350+
h.logger.Error().Err(err).Int64("serverID", serverID).Msg("Error removing MCP server via API")
351+
// Return error response for HTMX
347352
if strings.Contains(err.Error(), "not found") {
348-
http.Redirect(w, r, "/error?error=Server not found", http.StatusSeeOther)
353+
w.WriteHeader(http.StatusNotFound)
354+
_, _ = w.Write([]byte(fmt.Sprintf("Server with ID %d not found", serverID)))
349355
} else {
350-
http.Redirect(w, r, "/error?error=Failed to remove server", http.StatusSeeOther)
356+
w.WriteHeader(http.StatusInternalServerError)
357+
_, _ = w.Write([]byte("Internal server error removing server"))
351358
}
352359
return
353360
}
354361

355-
http.Redirect(w, r, "/success", http.StatusSeeOther)
362+
// Success: Return 200 OK. List update will happen via SSE.
363+
w.WriteHeader(http.StatusOK)
364+
// No body needed
356365
}
357366

358367
// --- Tool Management Handlers ---

internal/web/handlers/sse.go

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -193,27 +193,47 @@ func (h *SSEHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
193193

194194
// listenToEvents subscribes to the event bus and processes events.
195195
func (h *SSEHandler) listenToEvents() {
196-
// Subscribe to specific events using handler functions
197-
h.bus.Subscribe(events.ServerAdded, h.handleEvent)
198-
h.bus.Subscribe(events.ServerRemoved, h.handleEvent)
199-
h.bus.Subscribe(events.ServerStatusChanged, h.handleEvent)
196+
// Subscribe to the consolidated ServerDataUpdated event
197+
h.bus.Subscribe(events.ServerDataUpdated, h.handleServerDataUpdated)
198+
// Remove individual subscriptions
199+
// h.bus.Subscribe(events.ServerAdded, h.handleEvent)
200+
// h.bus.Subscribe(events.ServerRemoved, h.handleEvent)
201+
// h.bus.Subscribe(events.ServerStatusChanged, h.handleEvent)
200202
// h.bus.Subscribe(events.ToolsUpdated, h.handleEvent) // Add if needed
201203

202-
h.logger.Info().Msg("SSE handler subscribed to event bus")
204+
h.logger.Info().Msg("SSE handler subscribed to ServerDataUpdated events")
203205

204206
// Keep the goroutine alive until the handler context is cancelled
205207
<-h.ctx.Done()
206208
h.logger.Info().Msg("SSE event listener stopping due to context cancellation.")
207209
}
208210

211+
// handleServerDataUpdated processes the ServerDataUpdated event and triggers a broadcast.
212+
func (h *SSEHandler) handleServerDataUpdated(event events.Event) {
213+
// Type assert to check if it's the correct event type (optional but good practice)
214+
if _, ok := event.(*events.ServerDataUpdatedEvent); !ok {
215+
h.logger.Warn().Str("eventType", string(event.Type())).Msg("Received unexpected event type in handleServerDataUpdated")
216+
return
217+
}
218+
219+
h.logger.Debug().Str("eventType", string(event.Type())).Msg("SSE handler processing ServerDataUpdated event")
220+
// Trigger a generic refresh broadcast
221+
go h.broadcastRefresh()
222+
}
223+
209224
// handleEvent is called by the event bus dispatcher when a subscribed event occurs.
225+
// DEPRECATED: Use handleServerDataUpdated instead.
226+
/*
210227
func (h *SSEHandler) handleEvent(event events.Event) {
211228
h.logger.Debug().Str("eventType", string(event.Type())).Msg("SSE handler processing event")
212229
// Process and broadcast in a separate goroutine to avoid blocking the event bus dispatcher
213230
go h.processAndBroadcastEvent(event)
214231
}
232+
*/
215233

216234
// processAndBroadcastEvent prepares and broadcasts an SSE message for a given event.
235+
// DEPRECATED: Use broadcastRefresh instead.
236+
/*
217237
func (h *SSEHandler) processAndBroadcastEvent(event events.Event) {
218238
var sseEventName string = "refresh-list" // Use a generic event name
219239
var serverID int64 // Declare without initial assignment
@@ -248,13 +268,21 @@ func (h *SSEHandler) processAndBroadcastEvent(event events.Event) {
248268
// Including the server ID in the data is optional but can be useful for debugging on the client.
249269
// For simplicity, we can just send an empty data payload.
250270
// sseMsg := h.formatSSEMessage(sseEventName, []byte(fmt.Sprintf(`{"serverId": %d}`, serverID)))
251-
sseMsg := h.formatSSEMessage(sseEventName, []byte("")) // Empty data payload
271+
sseMsg := h.formatSSEMessage(sseEventName, nil)
272+
h.broadcast <- sseMsg
273+
h.logger.Info().Str("eventName", sseEventName).Msg("Broadcasted SSE message")
274+
}
275+
*/
252276

253-
h.logger.Info().Str("eventName", sseEventName).Msg("Broadcasting SSE refresh trigger")
277+
// broadcastRefresh formats and sends a generic 'refresh-list' SSE message.
278+
func (h *SSEHandler) broadcastRefresh() {
279+
sseEventName := "refresh-list"
280+
sseMsg := h.formatSSEMessage(sseEventName, nil) // No data needed for generic refresh
254281
h.broadcast <- sseMsg
282+
h.logger.Info().Str("eventName", sseEventName).Msg("Broadcasted SSE refresh message")
255283
}
256284

257-
// formatSSEMessage formats data into a valid SSE message string.
285+
// formatSSEMessage constructs a message in SSE format.
258286
func (h *SSEHandler) formatSSEMessage(eventName string, data []byte) []byte {
259287
var msg bytes.Buffer
260288
fmt.Fprintf(&msg, "event: %s\n", eventName)

0 commit comments

Comments
 (0)