package webui import ( "embed" "io/fs" "net/http" "strings" ) //go:embed all:dist var distFS embed.FS // Handler returns an http.Handler that serves the embedded frontend bundle // (web/dist copied into ./dist by the Dockerfile). Unmatched paths fall back // to /index.html so TanStack Router's client-side routes (/login, /setup, // /oauth/callback, …) work on hard-refresh. // // In local dev the embed FS is empty (only .gitkeep), so this returns a 404 // for everything; the frontend is served by `vite dev` on :5173 instead. func Handler() http.Handler { sub, err := fs.Sub(distFS, "dist") if err != nil { // Should be unreachable; the embed dir always exists. panic(err) } fileServer := http.FileServer(http.FS(sub)) return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { path := strings.TrimPrefix(r.URL.Path, "/") if path == "" { path = "index.html" } // Probe the file. If absent, rewrite to "/" so FileServer serves the // SPA index.html. served := path f, err := sub.Open(path) if err != nil { r.URL.Path = "/" served = "index.html" } else { _ = f.Close() } // 缓存策略:embed.FS 无 modtime → 默认不带任何缓存头,浏览器每次都重取 // (表现为登录跳转回来后 logo 等图片闪烁重载)。按资源类型显式分级: // - /assets/* 是 Vite 带内容哈希的 bundle → 永久不可变缓存 // - index.html(含 SPA 回退)必须每次校验,才能在新部署后拉到新哈希 // - 其余静态资源(logo / favicon / manifest)极少变 → 一周缓存, // 使 logo 仅首访下载一次、后续导航与跨页直接命中缓存、不再重载 switch { case strings.HasPrefix(served, "assets/"): w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") case served == "index.html": w.Header().Set("Cache-Control", "no-cache") default: w.Header().Set("Cache-Control", "public, max-age=604800") } fileServer.ServeHTTP(w, r) }) }