Show AI cost refresh errors and harden log redaction
This commit is contained in:
parent
877a4f08e7
commit
3ea6c1be5d
9 changed files with 52 additions and 23 deletions
|
|
@ -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">
|
||||||
|
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()}>
|
<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>
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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]
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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()
|
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")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue