diff --git a/README.md b/README.md index 7b5e221..ac0f99f 100644 --- a/README.md +++ b/README.md @@ -229,6 +229,8 @@ changes the config rolls the pod. The MCP endpoint is off in the chart too. `mcp.enabled` and `mcp.publicURL` turn it on; the chart refuses to render it with `authBypass`. See [`docs/mcp.md`](docs/mcp.md#with-the-helm-chart). +Setting `CRYPTOS_DEV_UI_ISSUE_COPY=true` adds a `` tag to the served index page, which lets a development console build show its "Copy for UI issue" button. Off by default; a release console build has no such button regardless of this setting. + ## 🔌 MCP endpoint The manager can serve a [Model Context Protocol](https://modelcontextprotocol.io) endpoint at `/mcp` for AI agents. It is off by default. diff --git a/cmd/manager/main.go b/cmd/manager/main.go index 948e3f7..3f26a71 100644 --- a/cmd/manager/main.go +++ b/cmd/manager/main.go @@ -231,7 +231,11 @@ func main() { connect.WithInterceptors(apperr.Interceptor()), ) - web, err := webui.Handler() + devUIIssueCopy := os.Getenv("CRYPTOS_DEV_UI_ISSUE_COPY") == "true" + if devUIIssueCopy { + log.Printf("manager: CRYPTOS_DEV_UI_ISSUE_COPY is set, so the console's dev UI-issue button is enabled") + } + web, err := webui.Handler(webui.Options{DevUIIssueCopy: devUIIssueCopy}) if err != nil { log.Fatalf("manager: webui: %v", err) } diff --git a/internal/webui/embed.go b/internal/webui/embed.go index 830f1ad..39a4d1f 100644 --- a/internal/webui/embed.go +++ b/internal/webui/embed.go @@ -17,6 +17,7 @@ limitations under the License. */ import ( + "bytes" "embed" "io/fs" "net/http" @@ -25,9 +26,19 @@ import ( //go:embed all:dist var distFS embed.FS +// Options tunes the web bundle handler. +type Options struct { + // DevUIIssueCopy adds the meta tag that lets a development console build + // show its "Copy for UI issue" button. Release builds don't contain the + // button, so the tag does nothing there. + DevUIIssueCopy bool +} + +const devUIIssueMeta = `` + // Handler serves the embedded web bundle. Requests that don't map to a file // fall back to index.html so the client-side router can handle the route. -func Handler() (http.Handler, error) { +func Handler(opts Options) (http.Handler, error) { sub, err := fs.Sub(distFS, "dist") if err != nil { return nil, err @@ -37,6 +48,9 @@ func Handler() (http.Handler, error) { if err != nil { return nil, err } + if opts.DevUIIssueCopy { + index = bytes.Replace(index, []byte(""), []byte(devUIIssueMeta+""), 1) + } return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/" { serveIndex(w, index) diff --git a/internal/webui/embed_test.go b/internal/webui/embed_test.go index ccdc7af..ba63907 100644 --- a/internal/webui/embed_test.go +++ b/internal/webui/embed_test.go @@ -24,7 +24,7 @@ import ( ) func TestHandler_ServesIndex(t *testing.T) { - h, err := Handler() + h, err := Handler(Options{}) if err != nil { t.Fatalf("Handler: %v", err) } @@ -36,10 +36,28 @@ func TestHandler_ServesIndex(t *testing.T) { } func TestHandler_SPAFallback(t *testing.T) { - h, _ := Handler() + h, _ := Handler(Options{}) rec := httptest.NewRecorder() h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/fleet", nil)) // no such file if rec.Code != http.StatusOK || !strings.Contains(rec.Body.String(), "CryptOS Fleet Manager") { t.Fatalf("GET /fleet (SPA) = %d, want index.html", rec.Code) } } + +func TestHandlerInjectsDevUIIssueMetaOnlyWhenEnabled(t *testing.T) { + for _, tc := range []struct { + on bool + want bool + }{{false, false}, {true, true}} { + h, err := Handler(Options{DevUIIssueCopy: tc.on}) + if err != nil { + t.Fatal(err) + } + rec := httptest.NewRecorder() + h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/fleet", nil)) + got := strings.Contains(rec.Body.String(), ``) + if got != tc.want { + t.Fatalf("on=%v: meta present=%v, want %v", tc.on, got, tc.want) + } + } +}