// Package httpserver assembles ori's HTTP surface: the embedded single-page // application, a health check, and (from later steps) the WebSocket bridge to // the ACP agent. package httpserver import ( "io/fs" "net/http" ) // Handler builds the root http.Handler of the ori server. // // uiFS is the root of the built SPA (index.html at its root, see ui.Dist). // extra maps additional route patterns (http.ServeMux syntax) to handlers and // is how the WebSocket endpoint plugs in without this package importing it. // // Example: // // h := httpserver.Handler(ui.Dist(), map[string]http.Handler{ // "GET /ws": wsHandler, // }) // log.Fatal(http.ListenAndServe(":8888", h)) func Handler(uiFS fs.FS, extra map[string]http.Handler) http.Handler { mux := http.NewServeMux() mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"status":"ok"}`)) }) for pattern, handler := range extra { mux.Handle(pattern, handler) } mux.Handle("/", spaHandler(uiFS)) return mux } // spaHandler serves the static files of the SPA and falls back to index.html // for any unknown path, so client-side routes deep-link correctly. func spaHandler(uiFS fs.FS) http.Handler { fileServer := http.FileServerFS(uiFS) return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if _, err := fs.Stat(uiFS, "index.html"); err != nil { http.Error(w, "UI not built: run `make build-ui` and rebuild the server", http.StatusServiceUnavailable) return } path := r.URL.Path if path != "/" { if _, err := fs.Stat(uiFS, path[1:]); err != nil { // Unknown path: let the SPA router handle it. r.URL.Path = "/" } } fileServer.ServeHTTP(w, r) }) }