// Package web serves the embedded admin console as a single-page app. // // The assets are compiled into the binary, so a deployment is one file plus a // config; there is no node process in production and no CDN dependency. When // the assets are absent (a `go build` without `npm run build`) the server says // so plainly instead of returning a confusing 404. package web import ( "embed" "fmt" "io/fs" "net/http" "strconv" "strings" ) //go:embed all:dist var dist embed.FS // Available reports whether compiled assets are present in this binary. func Available() bool { _, err := dist.Open("dist/index.html") return err == nil } // Handler serves the console, falling back to index.html for client-side // routes so a deep link like /logs works on a hard refresh. func Handler(basePath string) (http.Handler, error) { sub, err := fs.Sub(dist, "dist") if err != nil { return nil, fmt.Errorf("web: open assets: %w", err) } if _, err := fs.Stat(sub, "index.html"); err != nil { return http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "text/html; charset=utf-8") w.WriteHeader(http.StatusServiceUnavailable) _, _ = w.Write([]byte(missingAssets)) }), nil } files := http.FileServer(http.FS(sub)) prefix := "/" if basePath != "" && basePath != "/" { prefix = "/" + strings.Trim(basePath, "/") + "/" } return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Hashed asset filenames are immutable, so they can be cached hard. if strings.HasPrefix(r.URL.Path, prefix+"assets/") { w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") } else { w.Header().Set("Cache-Control", "no-cache") } clean := strings.TrimPrefix(r.URL.Path, strings.TrimSuffix(prefix, "/")) clean = strings.TrimPrefix(clean, "/") if clean == "" { clean = "index.html" } if _, err := fs.Stat(sub, clean); err != nil { // Unknown path: hand it to the SPA router. b, rerr := fs.ReadFile(sub, "index.html") if rerr != nil { http.NotFound(w, r) return } w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Header().Set("Cache-Control", "no-cache") w.WriteHeader(http.StatusOK) _, _ = w.Write(b) return } files.ServeHTTP(w, r) }), nil } const missingAssets = `
The admin API is running, but the web console was not built into this binary.
Build it with make web (or cd web && npm ci && npm run build), then rebuild the binary.
The JSON API is available under /api.