Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 41 additions & 13 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"net/http"
"os"
"os/signal"
"sync/atomic"
"syscall"
"time"

Expand Down Expand Up @@ -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 != "/" {
Expand All @@ -52,28 +66,38 @@ 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/" {
c.JSON(http.StatusNotFound, gin.H{"error": "API endpoint not found"})
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("<!doctype html><html><body><p>index.html not found</p></body></html>")
}

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) {
Expand Down Expand Up @@ -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(),
Expand Down
10 changes: 10 additions & 0 deletions pkg/model/general_setting.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 == "" {
Expand Down Expand Up @@ -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
}

Expand Down
7 changes: 2 additions & 5 deletions pkg/utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package utils

import (
"fmt"
"regexp"
"strings"

"k8s.io/apimachinery/pkg/util/rand"
Expand All @@ -11,14 +10,12 @@ import (
func InjectAnalytics(htmlContent string) string {
analyticsScript := `<script defer src="https://cloud.umami.is/script.js" data-website-id="c3d8a914-abbc-4eed-9699-a9192c4bef9e" data-exclude-search="true" data-exclude-hash="true" data-do-not-track="true"></script>`

re := regexp.MustCompile(`</head>`)
return re.ReplaceAllString(htmlContent, " "+analyticsScript+"\n </head>")
return strings.Replace(htmlContent, "</head>", " "+analyticsScript+"\n </head>", 1)
}

func InjectKiteBase(htmlContent string, base string) string {
baseScript := fmt.Sprintf(`<script>window.__dynamic_base__='%s';</script>`, base)
re := regexp.MustCompile(`<head>`)
return re.ReplaceAllString(htmlContent, "<head>\n "+baseScript)
return strings.Replace(htmlContent, "<head>", "<head>\n "+baseScript, 1)
}

func RandomString(length int) string {
Expand Down