diff --git a/frontend-modern/src/components/AI/AICostDashboard.tsx b/frontend-modern/src/components/AI/AICostDashboard.tsx index 05b1e541d..f269d1d85 100644 --- a/frontend-modern/src/components/AI/AICostDashboard.tsx +++ b/frontend-modern/src/components/AI/AICostDashboard.tsx @@ -48,7 +48,9 @@ export const AICostDashboard: Component = () => { if (seq !== requestSeq) return; logger.error('[AICostDashboard] Failed to load cost summary:', err); notificationStore.error('Failed to load AI cost summary'); - setLoadError('Failed to load usage data'); + const message = + err instanceof Error && err.message ? err.message : 'Failed to load usage data'; + setLoadError(message); } finally { if (seq === requestSeq) setLoading(false); } @@ -136,6 +138,22 @@ export const AICostDashboard: Component = () => {
Loading usage…
+ +
+
+ Couldn’t refresh. Showing last loaded data. {loadError()} +
+ +
+
+
{loadError()}
diff --git a/internal/api/ai_handlers.go b/internal/api/ai_handlers.go index 794984dfd..1f7860c02 100644 --- a/internal/api/ai_handlers.go +++ b/internal/api/ai_handlers.go @@ -1554,7 +1554,7 @@ func (h *AISettingsHandler) HandleOAuthStart(w http.ResponseWriter, r *http.Requ authURL := providers.GetAuthorizationURL(session) log.Info(). - Str("state", session.State[:8]+"..."). + Str("state", safePrefixForLog(session.State, 8)+"..."). Str("verifier_len", fmt.Sprintf("%d", len(session.CodeVerifier))). Str("auth_url", authURL). Msg("Starting Claude OAuth flow - user must visit URL and paste code back") diff --git a/internal/api/auth.go b/internal/api/auth.go index aef1ee46c..e77328526 100644 --- a/internal/api/auth.go +++ b/internal/api/auth.go @@ -293,7 +293,7 @@ func CheckAuth(cfg *config.Config, w http.ResponseWriter, r *http.Request) bool } // Debug logging for failed session validation log.Debug(). - Str("session_token", cookie.Value[:8]+"..."). + Str("session_token", safePrefixForLog(cookie.Value, 8)+"..."). Str("path", r.URL.Path). Msg("Session validation failed - token not found or expired") } else if err != nil { @@ -419,7 +419,7 @@ func CheckAuth(cfg *config.Config, w http.ResponseWriter, r *http.Request) bool log.Debug(). Bool("secure", isSecure). Str("same_site", sameSiteName). - Str("token", token[:8]+"..."). + Str("token", safePrefixForLog(token, 8)+"..."). Str("remote_addr", r.RemoteAddr). Msg("Setting session cookie after successful login") diff --git a/internal/api/config_handlers.go b/internal/api/config_handlers.go index 2571c0f58..e0c653476 100644 --- a/internal/api/config_handlers.go +++ b/internal/api/config_handlers.go @@ -2429,12 +2429,12 @@ func (h *ConfigHandlers) HandleRefreshClusterNodes(w http.ResponseWriter, r *htt w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ - "status": "success", - "clusterName": pve.ClusterName, - "oldNodeCount": oldEndpointCount, - "newNodeCount": newEndpointCount, - "nodesAdded": newEndpointCount - oldEndpointCount, - "clusterNodes": clusterEndpoints, + "status": "success", + "clusterName": pve.ClusterName, + "oldNodeCount": oldEndpointCount, + "newNodeCount": newEndpointCount, + "nodesAdded": newEndpointCount - oldEndpointCount, + "clusterNodes": clusterEndpoints, }) } @@ -5374,7 +5374,7 @@ func (h *ConfigHandlers) HandleSetupScriptURL(w http.ResponseWriter, r *http.Req h.codeMutex.Unlock() log.Info(). - Str("token_hash", tokenHash[:8]+"..."). + Str("token_hash", safePrefixForLog(tokenHash, 8)+"..."). Time("expiry", expiry). Str("type", req.Type). Msg("Generated temporary auth token") @@ -5614,7 +5614,7 @@ func (h *ConfigHandlers) HandleAutoRegister(w http.ResponseWriter, r *http.Reque codeHash := internalauth.HashAPIToken(authCode) log.Debug(). Bool("hasAuthCode", true). - Str("codeHash", codeHash[:8]+"..."). + Str("codeHash", safePrefixForLog(codeHash, 8)+"..."). Msg("Checking auth token as setup code") h.codeMutex.Lock() setupCode, exists := h.setupCodes[codeHash] diff --git a/internal/api/csrf_store.go b/internal/api/csrf_store.go index 082dd0b2c..15a74fabc 100644 --- a/internal/api/csrf_store.go +++ b/internal/api/csrf_store.go @@ -158,7 +158,7 @@ func (c *CSRFTokenStore) cleanup() { for sessionKey, token := range c.tokens { if now.After(token.Expires) { delete(c.tokens, sessionKey) - log.Debug().Str("sessionKey", sessionKey[:8]+"...").Msg("Cleaned up expired CSRF token") + log.Debug().Str("sessionKey", safePrefixForLog(sessionKey, 8)+"...").Msg("Cleaned up expired CSRF token") } } } diff --git a/internal/api/log_redact.go b/internal/api/log_redact.go new file mode 100644 index 000000000..c8b019bdf --- /dev/null +++ b/internal/api/log_redact.go @@ -0,0 +1,11 @@ +package api + +func safePrefixForLog(value string, n int) string { + if n <= 0 || value == "" { + return "" + } + if len(value) <= n { + return value + } + return value[:n] +} diff --git a/internal/api/recovery_tokens.go b/internal/api/recovery_tokens.go index d72d7e8aa..d3ab0f8a4 100644 --- a/internal/api/recovery_tokens.go +++ b/internal/api/recovery_tokens.go @@ -83,7 +83,7 @@ func (r *RecoveryTokenStore) GenerateRecoveryToken(duration time.Duration) (stri r.saveUnsafe() log.Info(). - Str("token", tokenStr[:8]+"..."). + Str("token", safePrefixForLog(tokenStr, 8)+"..."). Time("expires", token.ExpiresAt). Msg("Recovery token generated") @@ -128,7 +128,7 @@ func (r *RecoveryTokenStore) ValidateRecoveryTokenConstantTime(providedToken str r.mu.RLock() log.Info(). - Str("token", tokenStr[:8]+"..."). + Str("token", safePrefixForLog(tokenStr, 8)+"..."). Str("ip", ip). Msg("Recovery token successfully validated") diff --git a/internal/api/security.go b/internal/api/security.go index 21a5bbc2d..3621f1fb3 100644 --- a/internal/api/security.go +++ b/internal/api/security.go @@ -61,7 +61,7 @@ func CheckCSRF(w http.ResponseWriter, r *http.Request) bool { log.Debug(). Str("path", r.URL.Path). Str("method", r.Method). - Str("session", cookie.Value[:8]+"..."). + Str("session", safePrefixForLog(cookie.Value, 8)+"..."). Bool("has_csrf_token", csrfToken != ""). Msg("CSRF validation attempt") @@ -69,12 +69,12 @@ func CheckCSRF(w http.ResponseWriter, r *http.Request) bool { if csrfToken == "" { log.Warn(). Str("path", r.URL.Path). - Str("session", cookie.Value[:8]+"..."). + Str("session", safePrefixForLog(cookie.Value, 8)+"..."). Msg("Missing CSRF token") clearCSRFCookie(w) if newToken := issueNewCSRFCookie(w, r, cookie.Value); newToken != "" { w.Header().Set("X-CSRF-Token", newToken) - log.Debug().Str("new_token", newToken[:8]+"...").Msg("Issued new CSRF token after missing") + log.Debug().Str("new_token", safePrefixForLog(newToken, 8)+"...").Msg("Issued new CSRF token after missing") } return false } @@ -83,20 +83,20 @@ func CheckCSRF(w http.ResponseWriter, r *http.Request) bool { if !validateCSRFToken(cookie.Value, csrfToken) { log.Warn(). Str("path", r.URL.Path). - Str("session", cookie.Value[:8]+"..."). - Str("provided_token", csrfToken[:8]+"..."). + Str("session", safePrefixForLog(cookie.Value, 8)+"..."). + Str("provided_token", safePrefixForLog(csrfToken, 8)+"..."). Msg("Invalid CSRF token") clearCSRFCookie(w) if newToken := issueNewCSRFCookie(w, r, cookie.Value); newToken != "" { w.Header().Set("X-CSRF-Token", newToken) - log.Debug().Str("new_token", newToken[:8]+"...").Msg("Issued new CSRF token after invalid") + log.Debug().Str("new_token", safePrefixForLog(newToken, 8)+"...").Msg("Issued new CSRF token after invalid") } return false } log.Debug(). Str("path", r.URL.Path). - Str("session", cookie.Value[:8]+"..."). + Str("session", safePrefixForLog(cookie.Value, 8)+"..."). Msg("CSRF validation successful") return true } diff --git a/internal/api/session_store.go b/internal/api/session_store.go index f2f35d626..3e03cc5d3 100644 --- a/internal/api/session_store.go +++ b/internal/api/session_store.go @@ -150,7 +150,7 @@ func (s *SessionStore) cleanup() { for key, session := range s.sessions { if now.After(session.ExpiresAt) { delete(s.sessions, key) - log.Debug().Str("sessionKey", key[:8]+"...").Msg("Cleaned up expired session") + log.Debug().Str("sessionKey", safePrefixForLog(key, 8)+"...").Msg("Cleaned up expired session") } } }