mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-04 16:13:47 +00:00
fix(usage): repair cost analytics and display precision - Automatic OpenRouter pricing sync with cost backfill for traces/snapshots/events - Live usage data merging for current-hour dashboard accuracy - 2-decimal API cost formatting across usage/overview pages - Comprehensive test coverage (PG, SQLite, HTTP, UI) Merged by github-maintain automation.
68 lines
1.8 KiB
Go
68 lines
1.8 KiB
Go
package webui
|
|
|
|
import (
|
|
"io/fs"
|
|
"net/http"
|
|
"strings"
|
|
)
|
|
|
|
// apiPrefixes are URL prefixes reserved for backend APIs.
|
|
// Requests matching these are never served by the SPA handler.
|
|
var apiPrefixes = []string{"/v1/", "/ws", "/health", "/mcp/"}
|
|
|
|
// Handler returns an http.Handler that serves the embedded SPA.
|
|
// Returns nil if no assets are embedded (built without embedui tag).
|
|
func Handler() http.Handler {
|
|
fsys := Assets()
|
|
if fsys == nil {
|
|
return nil
|
|
}
|
|
fileServer := http.FileServer(http.FS(fsys))
|
|
return &spaHandler{fs: fsys, fileServer: fileServer}
|
|
}
|
|
|
|
type spaHandler struct {
|
|
fs fs.FS
|
|
fileServer http.Handler
|
|
}
|
|
|
|
func (h *spaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
// Never intercept API routes.
|
|
for _, prefix := range apiPrefixes {
|
|
if strings.HasPrefix(r.URL.Path, prefix) {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
}
|
|
|
|
// Try to serve the file directly.
|
|
path := strings.TrimPrefix(r.URL.Path, "/")
|
|
if path == "" {
|
|
path = "index.html"
|
|
}
|
|
|
|
// Check if file exists in the embedded FS.
|
|
if _, err := fs.Stat(h.fs, path); err == nil {
|
|
// Static assets: set long cache for /assets/* (Vite hashed filenames).
|
|
if strings.HasPrefix(r.URL.Path, "/assets/") {
|
|
w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
|
|
} else {
|
|
setIndexCacheHeaders(w)
|
|
}
|
|
h.fileServer.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
|
|
// SPA fallback: serve index.html for any unmatched route.
|
|
// This handles client-side routing (React Router).
|
|
setIndexCacheHeaders(w)
|
|
r.URL.Path = "/"
|
|
h.fileServer.ServeHTTP(w, r)
|
|
}
|
|
|
|
func setIndexCacheHeaders(w http.ResponseWriter) {
|
|
// index.html and SPA fallback routes must never be cached so the browser
|
|
// always fetches the latest Vite asset manifest after an embedded UI rebuild.
|
|
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
|
}
|