forked from bots-garden/ori
| ✨ Introduce new feature(s): ACP web client — Go backend (agent, bridge, httpserver, mockagent) + React SPA (Zed-like agent panel), tests, quality gate PASS | 1 | // Package httpserver assembles ori's HTTP surface: the embedded single-page |
| 2 | // application, a health check, and (from later steps) the WebSocket bridge to | |
| 3 | // the ACP agent. | |
| 4 | package httpserver | |
| 5 | ||
| 6 | import ( | |
| 7 | "io/fs" | |
| 8 | "net/http" | |
| 9 | ) | |
| 10 | ||
| 11 | // Handler builds the root http.Handler of the ori server. | |
| 12 | // | |
| 13 | // uiFS is the root of the built SPA (index.html at its root, see ui.Dist). | |
| 14 | // extra maps additional route patterns (http.ServeMux syntax) to handlers and | |
| 15 | // is how the WebSocket endpoint plugs in without this package importing it. | |
| 16 | // | |
| 17 | // Example: | |
| 18 | // | |
| 19 | // h := httpserver.Handler(ui.Dist(), map[string]http.Handler{ | |
| 20 | // "GET /ws": wsHandler, | |
| 21 | // }) | |
| 22 | // log.Fatal(http.ListenAndServe(":8888", h)) | |
| 23 | func Handler(uiFS fs.FS, extra map[string]http.Handler) http.Handler { | |
| 24 | mux := http.NewServeMux() | |
| 25 | mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, _ *http.Request) { | |
| 26 | w.Header().Set("Content-Type", "application/json") | |
| 27 | _, _ = w.Write([]byte(`{"status":"ok"}`)) | |
| 28 | }) | |
| 29 | for pattern, handler := range extra { | |
| 30 | mux.Handle(pattern, handler) | |
| 31 | } | |
| 32 | mux.Handle("/", spaHandler(uiFS)) | |
| 33 | return mux | |
| 34 | } | |
| 35 | ||
| 36 | // spaHandler serves the static files of the SPA and falls back to index.html | |
| 37 | // for any unknown path, so client-side routes deep-link correctly. | |
| 38 | func spaHandler(uiFS fs.FS) http.Handler { | |
| 39 | fileServer := http.FileServerFS(uiFS) | |
| 40 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| 41 | if _, err := fs.Stat(uiFS, "index.html"); err != nil { | |
| 42 | http.Error(w, "UI not built: run `make build-ui` and rebuild the server", http.StatusServiceUnavailable) | |
| 43 | return | |
| 44 | } | |
| 45 | path := r.URL.Path | |
| 46 | if path != "/" { | |
| 47 | if _, err := fs.Stat(uiFS, path[1:]); err != nil { | |
| 48 | // Unknown path: let the SPA router handle it. | |
| 49 | r.URL.Path = "/" | |
| 50 | } | |
| 51 | } | |
| 52 | fileServer.ServeHTTP(w, r) | |
| 53 | }) | |
| 54 | } |