diff --git a/llm-gateway/cmd/gateway/main.go b/llm-gateway/cmd/gateway/main.go index 836781a..c8bbd06 100644 --- a/llm-gateway/cmd/gateway/main.go +++ b/llm-gateway/cmd/gateway/main.go @@ -141,6 +141,7 @@ func main() { // Audit logger auditLogger := storage.NewAuditLogger(db) + auditLogger.OnWrite = sseBroker.Notify authHandlers.SetAuditLogger(auditLogger) // Debug logger @@ -149,6 +150,7 @@ func main() { debugDataDir = filepath.Dir(cfg.Database.Path) } debugLogger := storage.NewDebugLogger(db, cfg.Debug.Enabled, debugDataDir) + debugLogger.OnWrite = sseBroker.Notify // Seed default admin seedDefaultAdmin(cfg, authStore) @@ -248,6 +250,11 @@ func main() { r.Post("/api/auth/setup", authHandlers.Setup) r.Post("/api/auth/login/totp", authHandlers.LoginTOTP) + // Favicon (prevent 401 noise in browser console) + r.Get("/favicon.ico", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNoContent) + }) + // Root redirect r.Get("/", func(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/dashboard", http.StatusFound) diff --git a/llm-gateway/internal/auth/handlers.go b/llm-gateway/internal/auth/handlers.go index 59ca0c7..7585587 100644 --- a/llm-gateway/internal/auth/handlers.go +++ b/llm-gateway/internal/auth/handlers.go @@ -6,6 +6,7 @@ import ( "encoding/hex" "encoding/json" "fmt" + "html/template" "net/http" "strconv" "strings" @@ -124,7 +125,13 @@ func (h *Handlers) Status(w http.ResponseWriter, r *http.Request) { } func (h *Handlers) Setup(w http.ResponseWriter, r *http.Request) { + htmx := isHTMX(r) + if h.store.HasAnyUser() { + if htmx { + writeHTMXError(w, "already initialized") + return + } writeError(w, http.StatusBadRequest, "already initialized") return } @@ -134,27 +141,48 @@ func (h *Handlers) Setup(w http.ResponseWriter, r *http.Request) { Password string `json:"password"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + if htmx { + writeHTMXError(w, "invalid request") + return + } writeError(w, http.StatusBadRequest, "invalid JSON") return } if req.Username == "" || req.Password == "" { + if htmx { + writeHTMXError(w, "username and password required") + return + } writeError(w, http.StatusBadRequest, "username and password required") return } if len(req.Password) < 8 { + if htmx { + writeHTMXError(w, "password must be at least 8 characters") + return + } writeError(w, http.StatusBadRequest, "password must be at least 8 characters") return } user, err := h.store.CreateUser(req.Username, req.Password, true) if err != nil { - writeError(w, http.StatusInternalServerError, "failed to create user: "+err.Error()) + msg := "failed to create user: " + err.Error() + if htmx { + writeHTMXError(w, msg) + return + } + writeError(w, http.StatusInternalServerError, msg) return } // Auto-login sessionID, err := h.store.CreateSession(user.ID, 7*24*time.Hour) if err != nil { + if htmx { + writeHTMXError(w, "failed to create session") + return + } writeError(w, http.StatusInternalServerError, "failed to create session") return } @@ -162,6 +190,10 @@ func (h *Handlers) Setup(w http.ResponseWriter, r *http.Request) { h.audit(r, "auth.setup", "user", fmt.Sprintf("%d", user.ID), "initial setup") h.setSessionCookie(w, sessionID) + if htmx { + writeHTMXRedirect(w, "/dashboard") + return + } writeJSON(w, map[string]any{ "user": map[string]any{ "id": user.ID, @@ -172,11 +204,17 @@ func (h *Handlers) Setup(w http.ResponseWriter, r *http.Request) { } func (h *Handlers) Login(w http.ResponseWriter, r *http.Request) { + htmx := isHTMX(r) + ip := r.RemoteAddr if fwd := r.Header.Get("X-Real-IP"); fwd != "" { ip = fwd } if !h.loginLimiter.allow(ip) { + if htmx { + writeHTMXError(w, "too many login attempts, try again in a minute") + return + } writeError(w, http.StatusTooManyRequests, "too many login attempts, try again in a minute") return } @@ -186,17 +224,29 @@ func (h *Handlers) Login(w http.ResponseWriter, r *http.Request) { Password string `json:"password"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + if htmx { + writeHTMXError(w, "invalid request") + return + } writeError(w, http.StatusBadRequest, "invalid JSON") return } user, err := h.store.GetUserByUsername(req.Username) if err != nil { + if htmx { + writeHTMXError(w, "invalid credentials") + return + } writeError(w, http.StatusUnauthorized, "invalid credentials") return } if !h.store.CheckPassword(user, req.Password) { + if htmx { + writeHTMXError(w, "invalid credentials") + return + } writeError(w, http.StatusUnauthorized, "invalid credentials") return } @@ -212,12 +262,27 @@ func (h *Handlers) Login(w http.ResponseWriter, r *http.Request) { SameSite: http.SameSiteLaxMode, MaxAge: 300, // 5 minutes }) + if htmx { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + fmt.Fprint(w, `
+
+ + +
+ +
`) + return + } writeJSON(w, map[string]any{"require_totp": true}) return } sessionID, err := h.store.CreateSession(user.ID, 7*24*time.Hour) if err != nil { + if htmx { + writeHTMXError(w, "failed to create session") + return + } writeError(w, http.StatusInternalServerError, "failed to create session") return } @@ -225,6 +290,10 @@ func (h *Handlers) Login(w http.ResponseWriter, r *http.Request) { h.audit(r, "auth.login", "user", fmt.Sprintf("%d", user.ID), user.Username) h.setSessionCookie(w, sessionID) + if htmx { + writeHTMXRedirect(w, "/dashboard") + return + } writeJSON(w, map[string]any{ "require_totp": false, "user": map[string]any{ @@ -236,14 +305,24 @@ func (h *Handlers) Login(w http.ResponseWriter, r *http.Request) { } func (h *Handlers) LoginTOTP(w http.ResponseWriter, r *http.Request) { + htmx := isHTMX(r) + cookie, err := r.Cookie("llmgw_pending") if err != nil || cookie.Value == "" { + if htmx { + writeHTMXError(w, "no pending login") + return + } writeError(w, http.StatusBadRequest, "no pending login") return } userID, err := h.verifyPendingToken(cookie.Value) if err != nil { + if htmx { + writeHTMXError(w, "invalid or expired pending login") + return + } writeError(w, http.StatusBadRequest, "invalid or expired pending login") return } @@ -252,17 +331,29 @@ func (h *Handlers) LoginTOTP(w http.ResponseWriter, r *http.Request) { Code string `json:"code"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + if htmx { + writeHTMXError(w, "invalid request") + return + } writeError(w, http.StatusBadRequest, "invalid JSON") return } user, err := h.store.GetUserByID(userID) if err != nil { + if htmx { + writeHTMXError(w, "user not found") + return + } writeError(w, http.StatusBadRequest, "user not found") return } if !ValidateTOTPCode(user.TOTPSecret, req.Code) { + if htmx { + writeHTMXError(w, "invalid TOTP code") + return + } writeError(w, http.StatusUnauthorized, "invalid TOTP code") return } @@ -278,11 +369,19 @@ func (h *Handlers) LoginTOTP(w http.ResponseWriter, r *http.Request) { sessionID, err := h.store.CreateSession(user.ID, 7*24*time.Hour) if err != nil { + if htmx { + writeHTMXError(w, "failed to create session") + return + } writeError(w, http.StatusInternalServerError, "failed to create session") return } h.setSessionCookie(w, sessionID) + if htmx { + writeHTMXRedirect(w, "/dashboard") + return + } writeJSON(w, map[string]any{ "user": map[string]any{ "id": user.ID, @@ -308,6 +407,10 @@ func (h *Handlers) Logout(w http.ResponseWriter, r *http.Request) { MaxAge: -1, }) + if isHTMX(r) { + writeHTMXRedirect(w, "/login") + return + } writeJSON(w, map[string]string{"status": "ok"}) } @@ -350,6 +453,7 @@ func (h *Handlers) TOTPSetup(w http.ResponseWriter, r *http.Request) { } func (h *Handlers) TOTPVerify(w http.ResponseWriter, r *http.Request) { + htmx := isHTMX(r) user := UserFromContext(r.Context()) if user == nil { writeError(w, http.StatusUnauthorized, "not authenticated") @@ -360,6 +464,10 @@ func (h *Handlers) TOTPVerify(w http.ResponseWriter, r *http.Request) { Code string `json:"code"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + if htmx { + writeHTMXError(w, "invalid request") + return + } writeError(w, http.StatusBadRequest, "invalid JSON") return } @@ -367,30 +475,53 @@ func (h *Handlers) TOTPVerify(w http.ResponseWriter, r *http.Request) { // Re-fetch user to get latest TOTP secret user, err := h.store.GetUserByID(user.ID) if err != nil { + if htmx { + writeHTMXError(w, "failed to fetch user") + return + } writeError(w, http.StatusInternalServerError, "failed to fetch user") return } if user.TOTPSecret == "" { + if htmx { + writeHTMXError(w, "TOTP not set up yet") + return + } writeError(w, http.StatusBadRequest, "TOTP not set up, call /api/auth/totp/setup first") return } if !ValidateTOTPCode(user.TOTPSecret, req.Code) { + if htmx { + writeHTMXError(w, "invalid TOTP code") + return + } writeError(w, http.StatusBadRequest, "invalid TOTP code") return } if err := h.store.EnableTOTP(user.ID); err != nil { + if htmx { + writeHTMXError(w, "failed to enable TOTP") + return + } writeError(w, http.StatusInternalServerError, "failed to enable TOTP") return } h.audit(r, "totp.enable", "user", fmt.Sprintf("%d", user.ID), "") + if htmx { + // Trigger settings page reload to show updated TOTP status + w.Header().Set("HX-Trigger", "settingsRefresh") + writeHTMXSuccess(w, "Two-factor authentication enabled") + return + } writeJSON(w, map[string]string{"status": "totp_enabled"}) } func (h *Handlers) TOTPDisable(w http.ResponseWriter, r *http.Request) { + htmx := isHTMX(r) user := UserFromContext(r.Context()) if user == nil { writeError(w, http.StatusUnauthorized, "not authenticated") @@ -398,11 +529,20 @@ func (h *Handlers) TOTPDisable(w http.ResponseWriter, r *http.Request) { } if err := h.store.DisableTOTP(user.ID); err != nil { + if htmx { + writeHTMXError(w, "failed to disable TOTP") + return + } writeError(w, http.StatusInternalServerError, "failed to disable TOTP") return } h.audit(r, "totp.disable", "user", fmt.Sprintf("%d", user.ID), "") + if htmx { + w.Header().Set("HX-Trigger", "settingsRefresh") + writeHTMXSuccess(w, "Two-factor authentication disabled") + return + } writeJSON(w, map[string]string{"status": "totp_disabled"}) } @@ -437,31 +577,53 @@ func (h *Handlers) ListUsers(w http.ResponseWriter, r *http.Request) { } func (h *Handlers) CreateUser(w http.ResponseWriter, r *http.Request) { + htmx := isHTMX(r) var req struct { Username string `json:"username"` Password string `json:"password"` IsAdmin bool `json:"is_admin"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + if htmx { + writeHTMXError(w, "invalid request") + return + } writeError(w, http.StatusBadRequest, "invalid JSON") return } if req.Username == "" || req.Password == "" { + if htmx { + writeHTMXError(w, "username and password required") + return + } writeError(w, http.StatusBadRequest, "username and password required") return } if len(req.Password) < 8 { + if htmx { + writeHTMXError(w, "password must be at least 8 characters") + return + } writeError(w, http.StatusBadRequest, "password must be at least 8 characters") return } user, err := h.store.CreateUser(req.Username, req.Password, req.IsAdmin) if err != nil { - writeError(w, http.StatusInternalServerError, "failed to create user: "+err.Error()) + msg := "failed to create user: " + err.Error() + if htmx { + writeHTMXError(w, msg) + return + } + writeError(w, http.StatusInternalServerError, msg) return } h.audit(r, "user.create", "user", fmt.Sprintf("%d", user.ID), user.Username) + if htmx { + writeHTMXRefresh(w) + return + } writeJSON(w, map[string]any{ "id": user.ID, "username": user.Username, @@ -470,9 +632,14 @@ func (h *Handlers) CreateUser(w http.ResponseWriter, r *http.Request) { } func (h *Handlers) DeleteUser(w http.ResponseWriter, r *http.Request) { + htmx := isHTMX(r) idStr := chi.URLParam(r, "id") id, err := strconv.ParseInt(idStr, 10, 64) if err != nil { + if htmx { + writeHTMXError(w, "invalid user ID") + return + } writeError(w, http.StatusBadRequest, "invalid user ID") return } @@ -480,16 +647,28 @@ func (h *Handlers) DeleteUser(w http.ResponseWriter, r *http.Request) { // Prevent deleting yourself user := UserFromContext(r.Context()) if user != nil && user.ID == id { + if htmx { + writeHTMXError(w, "cannot delete yourself") + return + } writeError(w, http.StatusBadRequest, "cannot delete yourself") return } if err := h.store.DeleteUser(id); err != nil { + if htmx { + writeHTMXError(w, err.Error()) + return + } writeError(w, http.StatusBadRequest, err.Error()) return } h.audit(r, "user.delete", "user", idStr, "") + if htmx { + writeHTMXRefresh(w) + return + } writeJSON(w, map[string]string{"status": "deleted"}) } @@ -520,6 +699,7 @@ func (h *Handlers) ListTokens(w http.ResponseWriter, r *http.Request) { } func (h *Handlers) CreateToken(w http.ResponseWriter, r *http.Request) { + htmx := isHTMX(r) user := UserFromContext(r.Context()) if user == nil { writeError(w, http.StatusUnauthorized, "not authenticated") @@ -532,10 +712,18 @@ func (h *Handlers) CreateToken(w http.ResponseWriter, r *http.Request) { DailyBudgetUSD float64 `json:"daily_budget_usd"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + if htmx { + writeHTMXError(w, "invalid request") + return + } writeError(w, http.StatusBadRequest, "invalid JSON") return } if req.Name == "" { + if htmx { + writeHTMXError(w, "name is required") + return + } writeError(w, http.StatusBadRequest, "name is required") return } @@ -546,11 +734,25 @@ func (h *Handlers) CreateToken(w http.ResponseWriter, r *http.Request) { plainKey, token, err := h.store.CreateAPIToken(user.ID, req.Name, req.RateLimitRPM, req.DailyBudgetUSD) if err != nil { - writeError(w, http.StatusInternalServerError, "failed to create token: "+err.Error()) + msg := "failed to create token: " + err.Error() + if htmx { + writeHTMXError(w, msg) + return + } + writeError(w, http.StatusInternalServerError, msg) return } h.audit(r, "token.create", "token", fmt.Sprintf("%d", token.ID), req.Name) + if htmx { + // Return HTML with the key display and trigger page refresh + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.Header().Set("HX-Trigger", "tokenCreated") + escaped := template.HTMLEscapeString(plainKey) + fmt.Fprintf(w, `
Token created! Copy the key below — it won't be shown again.
+
%s
`, escaped) + return + } writeJSON(w, map[string]any{ "key": plainKey, "token": token, @@ -558,6 +760,7 @@ func (h *Handlers) CreateToken(w http.ResponseWriter, r *http.Request) { } func (h *Handlers) DeleteToken(w http.ResponseWriter, r *http.Request) { + htmx := isHTMX(r) user := UserFromContext(r.Context()) if user == nil { writeError(w, http.StatusUnauthorized, "not authenticated") @@ -567,6 +770,10 @@ func (h *Handlers) DeleteToken(w http.ResponseWriter, r *http.Request) { idStr := chi.URLParam(r, "id") id, err := strconv.ParseInt(idStr, 10, 64) if err != nil { + if htmx { + writeHTMXError(w, "invalid token ID") + return + } writeError(w, http.StatusBadRequest, "invalid token ID") return } @@ -575,27 +782,44 @@ func (h *Handlers) DeleteToken(w http.ResponseWriter, r *http.Request) { if !user.IsAdmin { token, err := h.store.GetAPIToken(id) if err != nil { + if htmx { + writeHTMXError(w, "token not found") + return + } writeError(w, http.StatusNotFound, "token not found") return } if token.UserID != user.ID { + if htmx { + writeHTMXError(w, "not your token") + return + } writeError(w, http.StatusForbidden, "not your token") return } } if err := h.store.DeleteAPIToken(id); err != nil { + if htmx { + writeHTMXError(w, "failed to delete token") + return + } writeError(w, http.StatusInternalServerError, "failed to delete token") return } h.audit(r, "token.delete", "token", idStr, "") + if htmx { + writeHTMXRefresh(w) + return + } writeJSON(w, map[string]string{"status": "deleted"}) } // Self-service endpoints func (h *Handlers) ChangePassword(w http.ResponseWriter, r *http.Request) { + htmx := isHTMX(r) user := UserFromContext(r.Context()) if user == nil { writeError(w, http.StatusUnauthorized, "not authenticated") @@ -607,10 +831,18 @@ func (h *Handlers) ChangePassword(w http.ResponseWriter, r *http.Request) { NewPassword string `json:"new_password"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + if htmx { + writeHTMXError(w, "invalid request") + return + } writeError(w, http.StatusBadRequest, "invalid JSON") return } if req.NewPassword == "" || len(req.NewPassword) < 8 { + if htmx { + writeHTMXError(w, "new password must be at least 8 characters") + return + } writeError(w, http.StatusBadRequest, "new password must be at least 8 characters") return } @@ -618,25 +850,42 @@ func (h *Handlers) ChangePassword(w http.ResponseWriter, r *http.Request) { // Re-fetch user to get password hash user, err := h.store.GetUserByID(user.ID) if err != nil { + if htmx { + writeHTMXError(w, "failed to fetch user") + return + } writeError(w, http.StatusInternalServerError, "failed to fetch user") return } if !h.store.CheckPassword(user, req.CurrentPassword) { + if htmx { + writeHTMXError(w, "current password is incorrect") + return + } writeError(w, http.StatusUnauthorized, "current password is incorrect") return } if err := h.store.UpdatePassword(user.ID, req.NewPassword); err != nil { + if htmx { + writeHTMXError(w, "failed to update password") + return + } writeError(w, http.StatusInternalServerError, "failed to update password") return } h.audit(r, "password.change", "user", fmt.Sprintf("%d", user.ID), "") + if htmx { + writeHTMXSuccess(w, "Password updated") + return + } writeJSON(w, map[string]string{"status": "password_updated"}) } func (h *Handlers) ChangeUsername(w http.ResponseWriter, r *http.Request) { + htmx := isHTMX(r) user := UserFromContext(r.Context()) if user == nil { writeError(w, http.StatusUnauthorized, "not authenticated") @@ -647,10 +896,18 @@ func (h *Handlers) ChangeUsername(w http.ResponseWriter, r *http.Request) { NewUsername string `json:"new_username"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + if htmx { + writeHTMXError(w, "invalid request") + return + } writeError(w, http.StatusBadRequest, "invalid JSON") return } if req.NewUsername == "" { + if htmx { + writeHTMXError(w, "username is required") + return + } writeError(w, http.StatusBadRequest, "username is required") return } @@ -658,20 +915,33 @@ func (h *Handlers) ChangeUsername(w http.ResponseWriter, r *http.Request) { // Check uniqueness existing, err := h.store.GetUserByUsername(req.NewUsername) if err == nil && existing.ID != user.ID { + if htmx { + writeHTMXError(w, "username already taken") + return + } writeError(w, http.StatusConflict, "username already taken") return } if err := h.store.UpdateUsername(user.ID, req.NewUsername); err != nil { + if htmx { + writeHTMXError(w, "failed to update username") + return + } writeError(w, http.StatusInternalServerError, "failed to update username") return } h.audit(r, "username.change", "user", fmt.Sprintf("%d", user.ID), req.NewUsername) + if htmx { + writeHTMXSuccess(w, "Username updated") + return + } writeJSON(w, map[string]string{"status": "username_updated"}) } func (h *Handlers) ChangeEmail(w http.ResponseWriter, r *http.Request) { + htmx := isHTMX(r) user := UserFromContext(r.Context()) if user == nil { writeError(w, http.StatusUnauthorized, "not authenticated") @@ -682,15 +952,27 @@ func (h *Handlers) ChangeEmail(w http.ResponseWriter, r *http.Request) { Email string `json:"email"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + if htmx { + writeHTMXError(w, "invalid request") + return + } writeError(w, http.StatusBadRequest, "invalid JSON") return } if err := h.store.UpdateEmail(user.ID, req.Email); err != nil { + if htmx { + writeHTMXError(w, "failed to update email") + return + } writeError(w, http.StatusInternalServerError, "failed to update email") return } + if htmx { + writeHTMXSuccess(w, "Email updated") + return + } writeJSON(w, map[string]string{"status": "email_updated"}) } @@ -749,6 +1031,30 @@ func (h *Handlers) verifyPendingToken(token string) (int64, error) { return userID, nil } +func isHTMX(r *http.Request) bool { + return r.Header.Get("HX-Request") == "true" +} + +func writeHTMXError(w http.ResponseWriter, msg string) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + fmt.Fprintf(w, `
%s
`, template.HTMLEscapeString(msg)) +} + +func writeHTMXSuccess(w http.ResponseWriter, msg string) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + fmt.Fprintf(w, `
%s
`, template.HTMLEscapeString(msg)) +} + +func writeHTMXRedirect(w http.ResponseWriter, url string) { + w.Header().Set("HX-Redirect", url) + w.WriteHeader(http.StatusOK) +} + +func writeHTMXRefresh(w http.ResponseWriter) { + w.Header().Set("HX-Refresh", "true") + w.WriteHeader(http.StatusOK) +} + func writeJSON(w http.ResponseWriter, v any) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(v) diff --git a/llm-gateway/internal/dashboard/api.go b/llm-gateway/internal/dashboard/api.go index e1f4639..fe75a2d 100644 --- a/llm-gateway/internal/dashboard/api.go +++ b/llm-gateway/internal/dashboard/api.go @@ -721,19 +721,47 @@ func (s *StatsAPI) DebugLogByRequestID(w http.ResponseWriter, r *http.Request) { } // ValidateConfig validates the config file at the stored path. +// Returns HTML for HTMX requests, JSON otherwise. func (s *StatsAPI) ValidateConfig(w http.ResponseWriter, r *http.Request) { if s.configPath == "" { - w.WriteHeader(http.StatusInternalServerError) - writeJSON(w, map[string]any{"valid": false, "errors": []string{"config path not set"}}) + if r.Header.Get("HX-Request") == "true" { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.Write([]byte(`
Config path not set
`)) + } else { + w.WriteHeader(http.StatusInternalServerError) + writeJSON(w, map[string]any{"valid": false, "errors": []string{"config path not set"}}) + } return } data, err := os.ReadFile(s.configPath) if err != nil { - w.WriteHeader(http.StatusInternalServerError) - writeJSON(w, map[string]any{"valid": false, "errors": []string{"failed to read config: " + err.Error()}}) + msg := "failed to read config: " + err.Error() + if r.Header.Get("HX-Request") == "true" { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.Write([]byte(`
` + msg + `
`)) + } else { + w.WriteHeader(http.StatusInternalServerError) + writeJSON(w, map[string]any{"valid": false, "errors": []string{msg}}) + } return } errs := config.ValidateBytes(data) + + if r.Header.Get("HX-Request") == "true" { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + if len(errs) > 0 { + html := `
Configuration errors:
" + w.Write([]byte(html)) + } else { + w.Write([]byte(`
Configuration is valid.
`)) + } + return + } + if len(errs) > 0 { writeJSON(w, map[string]any{"valid": false, "errors": errs}) return diff --git a/llm-gateway/internal/dashboard/templates/layout.html b/llm-gateway/internal/dashboard/templates/layout.html index 4925906..aebd327 100644 --- a/llm-gateway/internal/dashboard/templates/layout.html +++ b/llm-gateway/internal/dashboard/templates/layout.html @@ -16,7 +16,6 @@ -

LLM Gateway

-
-
-
- - -
-
- - -
- -
- +
+
+
+
+ + +
+
+ + +
+ +
+
- {{end}} diff --git a/llm-gateway/internal/dashboard/templates/partials/dashboard.html b/llm-gateway/internal/dashboard/templates/partials/dashboard.html index 1e2d13b..715313b 100644 --- a/llm-gateway/internal/dashboard/templates/partials/dashboard.html +++ b/llm-gateway/internal/dashboard/templates/partials/dashboard.html @@ -1,5 +1,4 @@ {{define "content"}} -
@@ -228,5 +227,4 @@ function loadCostBreakdown(groupBy, btn) { } loadCostBreakdown('model'); -
{{end}} diff --git a/llm-gateway/internal/dashboard/templates/partials/debug.html b/llm-gateway/internal/dashboard/templates/partials/debug.html index ffb419b..4b55fd5 100644 --- a/llm-gateway/internal/dashboard/templates/partials/debug.html +++ b/llm-gateway/internal/dashboard/templates/partials/debug.html @@ -7,7 +7,11 @@
Debug Mode {{if .DebugEnabled}}Enabled — requests are being logged{{else}}Disabled{{end}} @@ -77,16 +81,6 @@
{{end}} diff --git a/llm-gateway/internal/dashboard/templates/partials/tokens.html b/llm-gateway/internal/dashboard/templates/partials/tokens.html index 58f85bd..9324c2a 100644 --- a/llm-gateway/internal/dashboard/templates/partials/tokens.html +++ b/llm-gateway/internal/dashboard/templates/partials/tokens.html @@ -4,10 +4,7 @@ - +

Static Tokens (from config, managed via environment variables)

@@ -70,7 +67,9 @@ {{formatTime .CreatedAt}} {{if gt .LastUsedAt 0}}{{formatTime .LastUsedAt}}{{else}}never{{end}} - + {{end}}{{end}} @@ -82,7 +81,8 @@