fix(dashboard): fall back to findDashboardDir when configured static dir is missing (bf-2qywd)

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 <noreply@anthropic.com>
This commit is contained in:
jedarden 2026-07-08 20:35:17 -04:00
parent 23876557bd
commit e3ef551297

View file

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