diff --git a/main.go b/main.go index 6d8a74f0..eb648586 100644 --- a/main.go +++ b/main.go @@ -9,6 +9,7 @@ import ( "net/http" "os" "os/signal" + "sync/atomic" "syscall" "time" @@ -37,6 +38,19 @@ import ( //go:embed static var static embed.FS +// cachedHTML holds the pre-processed index.html bytes. +// Written once at startup via RefreshProcessedHTML and again whenever the +// admin toggles analytics in General Settings, so the NoRoute handler +// always serves a fresh copy without any per-request work. +var cachedHTML atomic.Value // stores []byte + +// RefreshProcessedHTML rebuilds the processed index.html from the embedded +// file and the current runtime settings (common.Base, common.EnableAnalytics). +// It is safe to call from any goroutine. +func RefreshProcessedHTML() { + cachedHTML.Store(preprocessIndexHTML(common.Base)) +} + func setupStatic(r *gin.Engine) { base := common.Base if base != "" && base != "/" { @@ -52,6 +66,10 @@ func setupStatic(r *gin.Engine) { assetsGroup := r.Group(base + "/assets") assetsGroup.Use(middleware.StaticCache()) assetsGroup.StaticFS("/", http.FS(assertsFS)) + + // Pre-process index.html once at startup. + RefreshProcessedHTML() + r.NoRoute(func(c *gin.Context) { path := c.Request.URL.Path if len(path) >= len(base)+5 && path[len(base):len(base)+5] == "/api/" { @@ -59,21 +77,27 @@ func setupStatic(r *gin.Engine) { return } - content, err := static.ReadFile("static/index.html") - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to read index.html"}) - return - } + c.Data(http.StatusOK, "text/html; charset=utf-8", cachedHTML.Load().([]byte)) + }) +} - htmlContent := string(content) - htmlContent = utils.InjectKiteBase(htmlContent, base) - if common.EnableAnalytics { - htmlContent = utils.InjectAnalytics(htmlContent) - } +// preprocessIndexHTML reads the embedded index.html, injects the kite base +// script and (optionally) the analytics tag, and returns the final bytes. +// Called once at startup — the result is immutable and shared across requests. +func preprocessIndexHTML(base string) []byte { + content, err := static.ReadFile("static/index.html") + if err != nil { + klog.Warningf("Failed to read embedded index.html: %v (UI may not be bundled)", err) + return []byte("
index.html not found
") + } - c.Header("Content-Type", "text/html; charset=utf-8") - c.String(http.StatusOK, htmlContent) - }) + htmlContent := string(content) + htmlContent = utils.InjectKiteBase(htmlContent, base) + if common.EnableAnalytics { + htmlContent = utils.InjectAnalytics(htmlContent) + } + + return []byte(htmlContent) } func setupAPIRouter(r *gin.RouterGroup, cm *cluster.ClusterManager) { @@ -259,6 +283,10 @@ func main() { setupAPIRouter(base, cm) setupStatic(r) + // Wire the settings-changed callback so that toggling analytics in the + // admin UI immediately regenerates the cached index.html. + model.OnSettingsChanged = RefreshProcessedHTML + srv := &http.Server{ Addr: ":" + common.Port, Handler: r.Handler(), diff --git a/pkg/model/general_setting.go b/pkg/model/general_setting.go index 36ede60b..6e248862 100644 --- a/pkg/model/general_setting.go +++ b/pkg/model/general_setting.go @@ -17,6 +17,11 @@ const GeneralAIProviderOpenAI = "openai" const GeneralAIProviderAnthropic = "anthropic" const DefaultGeneralAIProvider = GeneralAIProviderOpenAI +// OnSettingsChanged is an optional callback invoked after UpdateGeneralSetting +// completes. main.go wires this to RefreshProcessedHTML so that changes to +// EnableAnalytics are reflected in the cached index.html without a restart. +var OnSettingsChanged func() + func DefaultGeneralNodeTerminalImageValue() string { image := strings.TrimSpace(common.NodeTerminalImage) if image == "" { @@ -132,6 +137,11 @@ func UpdateGeneralSetting(updates map[string]interface{}) (*GeneralSetting, erro return nil, err } applyRuntimeGeneralSetting(setting) + + // Notify listeners (e.g. refresh cached index.html) only on actual mutations. + if OnSettingsChanged != nil { + OnSettingsChanged() + } return setting, nil } diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index 96fe1e83..381b2228 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -2,7 +2,6 @@ package utils import ( "fmt" - "regexp" "strings" "k8s.io/apimachinery/pkg/util/rand" @@ -11,14 +10,12 @@ import ( func InjectAnalytics(htmlContent string) string { analyticsScript := `` - re := regexp.MustCompile(``) - return re.ReplaceAllString(htmlContent, " "+analyticsScript+"\n ") + return strings.Replace(htmlContent, "", " "+analyticsScript+"\n ", 1) } func InjectKiteBase(htmlContent string, base string) string { baseScript := fmt.Sprintf(``, base) - re := regexp.MustCompile(``) - return re.ReplaceAllString(htmlContent, "\n "+baseScript) + return strings.Replace(htmlContent, "", "\n "+baseScript, 1) } func RandomString(length int) string {