From e3ef551297e59d9bb07d7f7f1064396764595d66 Mon Sep 17 00:00:00 2001 From: jedarden Date: Wed, 8 Jul 2026 20:35:17 -0400 Subject: [PATCH] fix(dashboard): fall back to findDashboardDir when configured static dir is missing (bf-2qywd) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cfg.StaticDir defaults to "/dashboard" (a container path). The old filesystem-fallback branch only called findDashboardDir() when StaticDir was empty, so on the host os.Stat("/dashboard") failed and the handler was never registered — every /css, /js and index.html got chi's default 404 (text/plain), Chrome refused the assets as wrong MIME, and no dashboard JS ran (the bf-4do5y false pass). Now os.Stat the configured dir first and fall back to findDashboardDir() when it is missing, registering the ServeFile handler against whichever candidate resolves. Also add a repo-root-relative candidate to findDashboardDir() (resolve the binary and walk up for a dashboard/ dir) so it works from any CWD. Emit a single WARN only when all candidates miss. Verified at runtime from repo root with SPAXEL_STATIC_DIR unset: GET /css/tokens.css -> 200 text/css; GET /js/app.js -> 200 text/javascript; GET / -> 200 text/html; log shows [INFO] Serving dashboard from filesystem. Co-Authored-By: Claude --- mothership/cmd/mothership/main.go | 85 +++++++++++++++++++++---------- 1 file changed, 59 insertions(+), 26 deletions(-) diff --git a/mothership/cmd/mothership/main.go b/mothership/cmd/mothership/main.go index fbdbb15..a400d4e 100644 --- a/mothership/cmd/mothership/main.go +++ b/mothership/cmd/mothership/main.go @@ -285,11 +285,36 @@ func computeZoneQuality(zone zones.Zone, pm *sigproc.ProcessorManager, hc *healt } func findDashboardDir() string { + // CWD-relative candidates. "./dashboard" resolves when the binary runs from + // the repo root (where dashboard/ lives); "./../dashboard" resolves when run + // from the mothership/ subdirectory. "/app/dashboard" is the container path. for _, dir := range []string{"./dashboard", "./../dashboard", "/app/dashboard"} { if _, err := os.Stat(dir); err == nil { return dir } } + // Repo-root-relative candidate (CWD-independent): resolve the running + // binary's own directory and walk upward looking for a "dashboard" + // subdirectory. This finds dashboard/ when the binary lives somewhere inside + // the repo checkout but is invoked from an unrelated working directory + // (e.g. /tmp), where the CWD-relative candidates above do not resolve. + if exe, err := os.Executable(); err == nil { + if resolved, err := filepath.EvalSymlinks(exe); err == nil { + exe = resolved + } + dir := filepath.Dir(exe) + for i := 0; i < 8; i++ { + cand := filepath.Join(dir, "dashboard") + if info, err := os.Stat(cand); err == nil && info.IsDir() { + return cand + } + parent := filepath.Dir(dir) + if parent == dir { + break // reached filesystem root + } + dir = parent + } + } return "" } @@ -4599,34 +4624,42 @@ func main() { }) } } else { - // Fallback to filesystem-based serving for development - staticDir := cfg.StaticDir - if staticDir == "" { - staticDir = findDashboardDir() - } - if staticDir != "" { - if _, err := os.Stat(staticDir); err == nil { - log.Printf("[INFO] Serving dashboard from filesystem at %s", staticDir) - r.Get("/*", func(w http.ResponseWriter, r *http.Request) { - path := filepath.Join(staticDir, r.URL.Path) - if info, err := os.Stat(path); err == nil && info.IsDir() { - path = filepath.Join(path, "index.html") - } - if _, err := os.Stat(path); err == nil { - http.ServeFile(w, r, path) - return - } - if filepath.Ext(r.URL.Path) == "" { - http.ServeFile(w, r, filepath.Join(staticDir, "index.html")) - return - } - http.NotFound(w, r) - }) - } else { - log.Printf("[WARN] Dashboard directory not found: %s", staticDir) + // Fallback to filesystem-based serving for development. + // cfg.StaticDir defaults to "/dashboard" (a container path). When the + // binary runs on the host (e.g. from the repo root, where dashboard/ + // lives) that path does not exist, so os.Stat(cfg.StaticDir) fails. In + // that case fall back to findDashboardDir() and serve from whichever + // candidate exists, rather than giving up and returning chi's default + // 404 for every /css, /js and index.html request. Emit a single WARN + // only when ALL candidates are missing. + resolvedDir := "" + if cfg.StaticDir != "" { + if _, err := os.Stat(cfg.StaticDir); err == nil { + resolvedDir = cfg.StaticDir } + } + if resolvedDir == "" { + resolvedDir = findDashboardDir() + } + if resolvedDir != "" { + log.Printf("[INFO] Serving dashboard from filesystem at %s", resolvedDir) + r.Get("/*", func(w http.ResponseWriter, r *http.Request) { + path := filepath.Join(resolvedDir, r.URL.Path) + if info, err := os.Stat(path); err == nil && info.IsDir() { + path = filepath.Join(path, "index.html") + } + if _, err := os.Stat(path); err == nil { + http.ServeFile(w, r, path) + return + } + if filepath.Ext(r.URL.Path) == "" { + http.ServeFile(w, r, filepath.Join(resolvedDir, "index.html")) + return + } + http.NotFound(w, r) + }) } else { - log.Printf("[WARN] No dashboard directory found, static files not served") + log.Printf("[WARN] Dashboard directory not found: tried configured %q then CWD candidates ./dashboard, ./../dashboard, /app/dashboard, and an executable-relative dashboard/ search — static files not served", cfg.StaticDir) } }