diff --git a/cmd/server_foreground.go b/cmd/server_foreground.go
index 3fd6bdc5c..640b517f2 100644
--- a/cmd/server_foreground.go
+++ b/cmd/server_foreground.go
@@ -21,6 +21,7 @@ import (
"encoding/json"
"fmt"
"io"
+ "io/fs"
"log"
"log/slog"
"os"
@@ -330,7 +331,14 @@ func runServerStart(cmd *cobra.Command, args []string) error {
}
if !web.AssetsEmbedded && webAssetsDir == "" {
- slog.Warn("This binary was built without web assets. The web UI will not be available. Run 'make web' and rebuild to include the web frontend, or use --web-assets-dir.")
+ slog.Warn("This binary was built without web assets. The web UI will not be available. Run 'make all' (or 'make web && make build') to rebuild with web assets included, or use --web-assets-dir.")
+ } else if web.AssetsEmbedded && webAssetsDir == "" {
+ sub, err := fs.Sub(web.ClientAssets, "dist/client")
+ if err != nil {
+ slog.Error("Failed to create sub-filesystem from embedded assets. The web UI will not be available. Run 'make web && make build' to rebuild with web assets included, or use --web-assets-dir.", "error", err)
+ } else if _, err := fs.Stat(sub, "assets/main.js"); err != nil {
+ slog.Warn("Embedded web assets are incomplete (main.js missing). The web UI will not be available. Run 'make all' (or 'make web && make build') to rebuild with web assets included, or use --web-assets-dir.")
+ }
}
log.Printf("Starting Web Frontend on %s:%d", cfg.Hub.Host, webPort)
wg.Add(1)
diff --git a/pkg/hub/web.go b/pkg/hub/web.go
index c1f95facc..de8d66512 100644
--- a/pkg/hub/web.go
+++ b/pkg/hub/web.go
@@ -381,11 +381,11 @@ var noAssetsPage = `
Web UI Not Available
-
This Scion binary was not built from source with web assets included.
+
This hub binary was built without embedded web assets.
The Hub API is still fully operational.
To use the web UI, either:
-
1. Build from source: make build
+
1. Rebuild with assets: make all (or make web && make build)
2. Point to pre-built assets: --web-assets-dir /path/to/dist/client
@@ -475,11 +475,15 @@ func NewWebServer(cfg WebServerConfig) *WebServer {
} else if web.AssetsEmbedded {
sub, err := fs.Sub(web.ClientAssets, "dist/client")
if err != nil {
- slog.Error("Failed to create sub-filesystem from embedded assets", "error", err)
+ slog.Error("Failed to create sub-filesystem from embedded assets. Run 'make web && make build' to rebuild with web assets included, or use --web-assets-dir.", "error", err)
+ } else if _, err := fs.Stat(sub, "assets/main.js"); err != nil {
+ slog.Warn("Embedded web assets directory exists but main.js is missing. Run 'make all' (or 'make web && make build') to rebuild with web assets included, or use --web-assets-dir.")
} else {
ws.assets = sub
}
- slog.Info("Web server using embedded assets")
+ if ws.assets != nil {
+ slog.Info("Web server using embedded assets")
+ }
} else {
slog.Warn("No web assets available: build with embedded assets or use --web-assets-dir")
}
diff --git a/pkg/hub/web_test.go b/pkg/hub/web_test.go
index c9b4d1a4f..b929e4e8a 100644
--- a/pkg/hub/web_test.go
+++ b/pkg/hub/web_test.go
@@ -24,6 +24,7 @@ import (
"path/filepath"
"strings"
"testing"
+ "testing/fstest"
"time"
"github.com/GoogleCloudPlatform/scion/pkg/store"
@@ -39,7 +40,13 @@ type mockWebStore struct {
func newTestWebServer(t *testing.T, cfg WebServerConfig) *WebServer {
t.Helper()
- return NewWebServer(cfg)
+ ws := NewWebServer(cfg)
+ if ws.assets == nil && ws.assetsDisk == "" && cfg.AssetsDir == "" {
+ ws.assets = fstest.MapFS{
+ "assets/main.js": &fstest.MapFile{Data: []byte("// test stub")},
+ }
+ }
+ return ws
}
// newDevAuthWebServer creates a web server with dev-auth enabled for testing
@@ -52,7 +59,13 @@ func newDevAuthWebServer(t *testing.T, overrides ...func(*WebServerConfig)) *Web
for _, fn := range overrides {
fn(&cfg)
}
- return NewWebServer(cfg)
+ ws := NewWebServer(cfg)
+ if ws.assets == nil && ws.assetsDisk == "" {
+ ws.assets = fstest.MapFS{
+ "assets/main.js": &fstest.MapFile{Data: []byte("// test stub")},
+ }
+ }
+ return ws
}
func TestSPAShellHandler(t *testing.T) {
@@ -231,7 +244,7 @@ func TestSPAHandler_NoAssets_ServesErrorPage(t *testing.T) {
assert.Equal(t, http.StatusOK, resp.StatusCode)
assert.Contains(t, resp.Header.Get("Content-Type"), "text/html")
assert.Contains(t, html, "Web UI Not Available")
- assert.Contains(t, html, "not built from source")
+ assert.Contains(t, html, "built without embedded web assets")
assert.Contains(t, html, "Hub API")
assert.NotContains(t, html, "scion-app",
"should not render SPA shell when no assets are available")