Show AI cost refresh errors and harden log redaction

This commit is contained in:
rcourtman 2025-12-12 11:05:24 +00:00
parent 877a4f08e7
commit 3ea6c1be5d
9 changed files with 52 additions and 23 deletions

View file

@ -48,7 +48,9 @@ export const AICostDashboard: Component = () => {
if (seq !== requestSeq) return; if (seq !== requestSeq) return;
logger.error('[AICostDashboard] Failed to load cost summary:', err); logger.error('[AICostDashboard] Failed to load cost summary:', err);
notificationStore.error('Failed to load AI cost summary'); 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 { } finally {
if (seq === requestSeq) setLoading(false); if (seq === requestSeq) setLoading(false);
} }
@ -136,6 +138,22 @@ export const AICostDashboard: Component = () => {
<div class="text-sm text-gray-500 dark:text-gray-400">Loading usage</div> <div class="text-sm text-gray-500 dark:text-gray-400">Loading usage</div>
</Show> </Show>
<Show when={loadError() && summary()}>
<div class="flex items-center justify-between gap-3 text-xs px-3 py-2 rounded border border-amber-200 dark:border-amber-800/60 bg-amber-50 dark:bg-amber-900/20 text-amber-900 dark:text-amber-100">
<div class="truncate">
Couldnt refresh. Showing last loaded data. {loadError()}
</div>
<button
type="button"
disabled={loading()}
onClick={() => loadSummary(days())}
class={`shrink-0 px-2 py-1 rounded border border-amber-300 dark:border-amber-700 hover:bg-amber-100 dark:hover:bg-amber-900/40 ${loading() ? 'opacity-60 cursor-not-allowed' : ''}`}
>
Retry
</button>
</div>
</Show>
<Show when={!summary() && !loading() && loadError()}> <Show when={!summary() && !loading() && loadError()}>
<div class="text-sm text-gray-500 dark:text-gray-400">{loadError()}</div> <div class="text-sm text-gray-500 dark:text-gray-400">{loadError()}</div>
</Show> </Show>

View file

@ -1554,7 +1554,7 @@ func (h *AISettingsHandler) HandleOAuthStart(w http.ResponseWriter, r *http.Requ
authURL := providers.GetAuthorizationURL(session) authURL := providers.GetAuthorizationURL(session)
log.Info(). log.Info().
Str("state", session.State[:8]+"..."). Str("state", safePrefixForLog(session.State, 8)+"...").
Str("verifier_len", fmt.Sprintf("%d", len(session.CodeVerifier))). Str("verifier_len", fmt.Sprintf("%d", len(session.CodeVerifier))).
Str("auth_url", authURL). Str("auth_url", authURL).
Msg("Starting Claude OAuth flow - user must visit URL and paste code back") Msg("Starting Claude OAuth flow - user must visit URL and paste code back")

View file

@ -293,7 +293,7 @@ func CheckAuth(cfg *config.Config, w http.ResponseWriter, r *http.Request) bool
} }
// Debug logging for failed session validation // Debug logging for failed session validation
log.Debug(). log.Debug().
Str("session_token", cookie.Value[:8]+"..."). Str("session_token", safePrefixForLog(cookie.Value, 8)+"...").
Str("path", r.URL.Path). Str("path", r.URL.Path).
Msg("Session validation failed - token not found or expired") Msg("Session validation failed - token not found or expired")
} else if err != nil { } else if err != nil {
@ -419,7 +419,7 @@ func CheckAuth(cfg *config.Config, w http.ResponseWriter, r *http.Request) bool
log.Debug(). log.Debug().
Bool("secure", isSecure). Bool("secure", isSecure).
Str("same_site", sameSiteName). Str("same_site", sameSiteName).
Str("token", token[:8]+"..."). Str("token", safePrefixForLog(token, 8)+"...").
Str("remote_addr", r.RemoteAddr). Str("remote_addr", r.RemoteAddr).
Msg("Setting session cookie after successful login") Msg("Setting session cookie after successful login")

View file

@ -2429,12 +2429,12 @@ func (h *ConfigHandlers) HandleRefreshClusterNodes(w http.ResponseWriter, r *htt
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{ json.NewEncoder(w).Encode(map[string]interface{}{
"status": "success", "status": "success",
"clusterName": pve.ClusterName, "clusterName": pve.ClusterName,
"oldNodeCount": oldEndpointCount, "oldNodeCount": oldEndpointCount,
"newNodeCount": newEndpointCount, "newNodeCount": newEndpointCount,
"nodesAdded": newEndpointCount - oldEndpointCount, "nodesAdded": newEndpointCount - oldEndpointCount,
"clusterNodes": clusterEndpoints, "clusterNodes": clusterEndpoints,
}) })
} }
@ -5374,7 +5374,7 @@ func (h *ConfigHandlers) HandleSetupScriptURL(w http.ResponseWriter, r *http.Req
h.codeMutex.Unlock() h.codeMutex.Unlock()
log.Info(). log.Info().
Str("token_hash", tokenHash[:8]+"..."). Str("token_hash", safePrefixForLog(tokenHash, 8)+"...").
Time("expiry", expiry). Time("expiry", expiry).
Str("type", req.Type). Str("type", req.Type).
Msg("Generated temporary auth token") Msg("Generated temporary auth token")
@ -5614,7 +5614,7 @@ func (h *ConfigHandlers) HandleAutoRegister(w http.ResponseWriter, r *http.Reque
codeHash := internalauth.HashAPIToken(authCode) codeHash := internalauth.HashAPIToken(authCode)
log.Debug(). log.Debug().
Bool("hasAuthCode", true). Bool("hasAuthCode", true).
Str("codeHash", codeHash[:8]+"..."). Str("codeHash", safePrefixForLog(codeHash, 8)+"...").
Msg("Checking auth token as setup code") Msg("Checking auth token as setup code")
h.codeMutex.Lock() h.codeMutex.Lock()
setupCode, exists := h.setupCodes[codeHash] setupCode, exists := h.setupCodes[codeHash]

View file

@ -158,7 +158,7 @@ func (c *CSRFTokenStore) cleanup() {
for sessionKey, token := range c.tokens { for sessionKey, token := range c.tokens {
if now.After(token.Expires) { if now.After(token.Expires) {
delete(c.tokens, sessionKey) 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")
} }
} }
} }

View file

@ -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]
}

View file

@ -83,7 +83,7 @@ func (r *RecoveryTokenStore) GenerateRecoveryToken(duration time.Duration) (stri
r.saveUnsafe() r.saveUnsafe()
log.Info(). log.Info().
Str("token", tokenStr[:8]+"..."). Str("token", safePrefixForLog(tokenStr, 8)+"...").
Time("expires", token.ExpiresAt). Time("expires", token.ExpiresAt).
Msg("Recovery token generated") Msg("Recovery token generated")
@ -128,7 +128,7 @@ func (r *RecoveryTokenStore) ValidateRecoveryTokenConstantTime(providedToken str
r.mu.RLock() r.mu.RLock()
log.Info(). log.Info().
Str("token", tokenStr[:8]+"..."). Str("token", safePrefixForLog(tokenStr, 8)+"...").
Str("ip", ip). Str("ip", ip).
Msg("Recovery token successfully validated") Msg("Recovery token successfully validated")

View file

@ -61,7 +61,7 @@ func CheckCSRF(w http.ResponseWriter, r *http.Request) bool {
log.Debug(). log.Debug().
Str("path", r.URL.Path). Str("path", r.URL.Path).
Str("method", r.Method). Str("method", r.Method).
Str("session", cookie.Value[:8]+"..."). Str("session", safePrefixForLog(cookie.Value, 8)+"...").
Bool("has_csrf_token", csrfToken != ""). Bool("has_csrf_token", csrfToken != "").
Msg("CSRF validation attempt") Msg("CSRF validation attempt")
@ -69,12 +69,12 @@ func CheckCSRF(w http.ResponseWriter, r *http.Request) bool {
if csrfToken == "" { if csrfToken == "" {
log.Warn(). log.Warn().
Str("path", r.URL.Path). Str("path", r.URL.Path).
Str("session", cookie.Value[:8]+"..."). Str("session", safePrefixForLog(cookie.Value, 8)+"...").
Msg("Missing CSRF token") Msg("Missing CSRF token")
clearCSRFCookie(w) clearCSRFCookie(w)
if newToken := issueNewCSRFCookie(w, r, cookie.Value); newToken != "" { if newToken := issueNewCSRFCookie(w, r, cookie.Value); newToken != "" {
w.Header().Set("X-CSRF-Token", 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 return false
} }
@ -83,20 +83,20 @@ func CheckCSRF(w http.ResponseWriter, r *http.Request) bool {
if !validateCSRFToken(cookie.Value, csrfToken) { if !validateCSRFToken(cookie.Value, csrfToken) {
log.Warn(). log.Warn().
Str("path", r.URL.Path). Str("path", r.URL.Path).
Str("session", cookie.Value[:8]+"..."). Str("session", safePrefixForLog(cookie.Value, 8)+"...").
Str("provided_token", csrfToken[:8]+"..."). Str("provided_token", safePrefixForLog(csrfToken, 8)+"...").
Msg("Invalid CSRF token") Msg("Invalid CSRF token")
clearCSRFCookie(w) clearCSRFCookie(w)
if newToken := issueNewCSRFCookie(w, r, cookie.Value); newToken != "" { if newToken := issueNewCSRFCookie(w, r, cookie.Value); newToken != "" {
w.Header().Set("X-CSRF-Token", 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 return false
} }
log.Debug(). log.Debug().
Str("path", r.URL.Path). Str("path", r.URL.Path).
Str("session", cookie.Value[:8]+"..."). Str("session", safePrefixForLog(cookie.Value, 8)+"...").
Msg("CSRF validation successful") Msg("CSRF validation successful")
return true return true
} }

View file

@ -150,7 +150,7 @@ func (s *SessionStore) cleanup() {
for key, session := range s.sessions { for key, session := range s.sessions {
if now.After(session.ExpiresAt) { if now.After(session.ExpiresAt) {
delete(s.sessions, key) 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")
} }
} }
} }