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)
+ }
+ }
+}