summaryrefslogtreecommitdiff
path: root/internal/api/server.go
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-06-03 22:56:24 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-06-03 22:56:24 +0000
commitb28cfc6ff288d083f6c8e9c055b69bfcadbceccc (patch)
treefb5e16fa588494bb400126a3ea774ae7bd34be1a /internal/api/server.go
parent476a3136a9f55e151ae689cd098795ec865e7850 (diff)
feat: add --base-path flag so UI routes API calls to correct prefix
Allows running multiple instances (e.g. /claudomator and /claudomator-oss) behind a reverse proxy. The server rewrites the base-path meta tag in index.html at request time rather than baking it into the embed. Also adds --branch support to deploy script for isolated branch builds via git worktrees. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'internal/api/server.go')
-rw-r--r--internal/api/server.go36
1 files changed, 35 insertions, 1 deletions
diff --git a/internal/api/server.go b/internal/api/server.go
index 2dcbf77..afb80ad 100644
--- a/internal/api/server.go
+++ b/internal/api/server.go
@@ -1,9 +1,11 @@
package api
import (
+ "bytes"
"context"
"encoding/json"
"fmt"
+ "io/fs"
"log/slog"
"net/http"
"os"
@@ -60,6 +62,7 @@ type Server struct {
pushStore pushSubscriptionStore
dropsDir string
llm *llm.Client
+ basePath string // URL prefix the UI is mounted at, e.g. "/claudomator"
}
// SetAPIToken configures a bearer token that must be supplied to access the API.
@@ -89,6 +92,12 @@ func (s *Server) SetWorkspaceRoot(path string) {
s.workspaceRoot = path
}
+// SetBasePath sets the URL prefix the UI is served under (e.g. "/claudomator-oss").
+// The server rewrites the base-path meta tag in index.html at request time.
+func (s *Server) SetBasePath(p string) {
+ s.basePath = p
+}
+
// Pool returns the executor pool, for graceful shutdown by the caller.
func (s *Server) Pool() *executor.Pool { return s.pool }
@@ -180,7 +189,7 @@ func (s *Server) routes() {
s.mux.HandleFunc("GET /api/drops", s.handleListDrops)
s.mux.HandleFunc("GET /api/drops/{filename}", s.handleGetDrop)
s.mux.HandleFunc("POST /api/drops", s.handlePostDrop)
- s.mux.Handle("GET /", http.FileServerFS(webui.Files))
+ s.mux.HandleFunc("GET /", s.handleStaticFiles)
}
// forwardResults listens on the executor pool's result and started channels and broadcasts via WebSocket.
@@ -777,6 +786,31 @@ func (s *Server) handleGetExecution(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, exec)
}
+// handleStaticFiles serves embedded web UI files. For index.html it rewrites the
+// base-path meta tag so the UI knows which API prefix to use.
+func (s *Server) handleStaticFiles(w http.ResponseWriter, r *http.Request) {
+ if r.URL.Path == "/" || r.URL.Path == "/index.html" {
+ raw, err := fs.ReadFile(webui.Files, "index.html")
+ if err != nil {
+ http.Error(w, "not found", http.StatusNotFound)
+ return
+ }
+ basePath := s.basePath
+ if basePath == "" {
+ basePath = "/claudomator"
+ }
+ rewritten := bytes.ReplaceAll(raw,
+ []byte(`content="/claudomator"`),
+ []byte(`content="`+basePath+`"`),
+ )
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ w.WriteHeader(http.StatusOK)
+ _, _ = w.Write(rewritten)
+ return
+ }
+ http.FileServerFS(webui.Files).ServeHTTP(w, r)
+}
+
func writeJSON(w http.ResponseWriter, status int, v interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)