mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-04-28 03:20:11 +00:00
Show AI cost refresh errors and harden log redaction
This commit is contained in:
parent
716a0b8c4d
commit
60c980a921
9 changed files with 52 additions and 23 deletions
|
|
@ -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 = () => {
|
|||
<div class="text-sm text-gray-500 dark:text-gray-400">Loading usage…</div>
|
||||
</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">
|
||||
Couldn’t 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()}>
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">{loadError()}</div>
|
||||
</Show>
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
11
internal/api/log_redact.go
Normal file
11
internal/api/log_redact.go
Normal 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]
|
||||
}
|
||||
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue