Skip to content

Surface credential sync failures in the management UI - #53

Merged
maiphucgiang merged 1 commit into
maiphucgiang:mainfrom
Good-design-999:patch/credential-error-visibility
Oct 4, 2026
Merged

maiphucgiang merged 1 commit into
maiphucgiang:mainfrom
Good-design-999:patch/credential-error-visibility

Conversation

@Good-design-999

@Good-design-999 Good-design-999 commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Summary

A credential whose sync or token refresh fails is visually indistinguishable from a healthy disabled account: the dashboard shows 已停用 and manual actions only return a generic "操作失败" message. The failure reason is recorded by the backend but never reaches the UI. This change exposes it.

Real-world reproduction

  1. A cn-cli account's refresh token was rejected by upstream:
    12153: refresh token failed: 400 Bad Request: invalid_grant: Refresh token issued before the user session started
  2. Clicking 同步 (sync) in the dashboard repeatedly failed; the only feedback was:
    操作失败,保留已有数据;请检查账号状态后重试
  3. The credential row showed 已停用 with no error indicator. The detailed reason
    (credits: AuthExpiredError: 登录身份过期) was stored in the credits ledger but never surfaced anywhere in the UI.
  4. Consequence: the operator cannot tell "manually disabled, healthy" apart from "manually disabled because sync kept failing" — the exact reason why the refresh/sync buttons "do nothing" is invisible.

Root cause

  1. Management.admin_credential_inventory (app/gateway_management.py) derives last_error_code / last_failure_at from entry["last_error"], but the credits ledger's detailed error (written by ledger.note_error via _sync_error) is never exposed. Note sync_error is already listed in the public credential field whitelist in app/admin_api.py — it was simply never populated.
  2. The web UI never renders last_error_code, last_failure_at or sync_error — the Chinese labels exist in web/src/values.tsx, but no JSX uses them (web/src/pages/Credentials.tsx).
  3. Manual credential actions collapse every exception into one generic message (app/credential_actions.py).

Changes (3 files, +23 / −2)

| File | Change |
|


|


|
| app/gateway_management.py | Populate sync_error from the ledger's error (fallback: entry["last_error"]). |
| web/src/pages/Credentials.tsx | Render 最近错误 (friendly labels for http_401 / http_403 / credential_error) with timestamp, plus the detailed 失败原因 under 认证健康. |
| app/credential_actions.py | Include the exception type in manual action failure messages. |

Deliberately unchanged: health precedence (disabled still wins over error) — the failure reason is shown as supplementary text, so existing status semantics are untouched.

Verification

  • Python: test_credential_* / test_admin_* suites pass (63 tests OK). Full suite: 64/65 files. The one failing file (test_stream_status_contract.py) is flaky on macOS without this change too (random 1–3 failures per run) and covers streaming, not credentials.
  • Frontend: vp build succeeds. Vitest failure list is identical to the unmodified baseline (18 pre-existing environment failures, same list before/after).
  • End-to-end (isolated instance, real ledger data): unmodified service returns no sync_error; patched service returns sync_error: "credits: AuthExpiredError: 登录身份过期".

API evidence

Before (unmodified):

{ "health": "disabled", "last_error_code": "credential_error", "last_failure_at": 1791041902.6 }

After (patched):

{ "health": "disabled", "last_error_code": "credential_error", "last_failure_at": 1791041902.6,
  "sync_error": "credits: AuthExpiredError: 登录身份过期" }

Summary by Sourcery

Surface credential synchronization failures and their underlying causes in the management experience.

New Features:

  • Expose credential synchronization and token refresh failure details in the management UI, including categorized errors, timestamps, and backend-provided reasons.

Bug Fixes:

  • Make manual credential action failures more informative by including the exception type instead of only a generic failure message.

Enhancements:

  • Propagate detailed credential sync errors from the ledger through the management API while preserving existing health status semantics.

A failed credential sync only produced a generic "操作失败" message and
the dashboard never rendered the recorded reason, so an account whose
refresh token was rejected looked identical to a healthy disabled one.

- Expose the ledger's last sync error as sync_error in the credential
  inventory (field was already whitelisted, never populated).
- Show last_error_code / last_failure_at and sync_error under 认证健康.
- Include the exception type in manual action failures instead of the
  generic message only.
@sourcery-ai

sourcery-ai Bot commented Oct 3, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

The PR carries credential synchronization errors from the backend ledger into the management API and renders both classified recent errors and detailed reasons in the UI, while adding safe exception-type context to manual action failures; disabled health precedence and existing status semantics remain unchanged.

Sequence diagram for surfacing credential sync failures

sequenceDiagram
    participant Ledger as CreditsLedger
    participant Management as ManagementAPI
    participant UI as CredentialsUI
    participant Operator

    Ledger->>Management: balance.error
    Management->>Management: admin_credential_inventory()
    Management-->>UI: sync_error, last_error_code, last_failure_at, health
    UI->>UI: lastErrorLabel(last_error_code)
    UI-->>Operator: 最近错误 + 时间戳
    UI-->>Operator: 失败原因(sync_error)
Loading

Sequence diagram for manual action failure feedback

sequenceDiagram
    participant Operator
    participant Actions as CredentialActions
    participant Upstream

    Operator->>Actions: run(gateway, action, identity)
    Actions->>Upstream: credential sync or refresh
    Upstream-->>Actions: Exception
    Actions->>Actions: type(error).__name__
    Actions-->>Operator: 操作失败(异常类型),保留已有数据
Loading

File-Level Changes

Change Details Files
Propagate detailed credential sync failures through the management inventory API.
  • Populate the public sync_error field from the ledger error, falling back to the existing entry error.
  • Preserve existing error-code and disabled-health semantics.
app/gateway_management.py
Expose failure classification, timing, and detailed reasons in the credential management UI.
  • Render localized labels for recent error codes with failure timestamps.
  • Render the detailed synchronization failure reason beneath credential health.
web/src/pages/Credentials.tsx
Make manual credential action failures more diagnostically useful without exposing sensitive exception text.
  • Include the exception class name in the generic failure response.
  • Retain existing result handling and data-preservation behavior.
app/credential_actions.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've reviewed your changes and they look great!

Sourcery assessment

Needs a human reviewer. The UI now renders balance.error or last_error directly, so a malformed upstream error could expose credential paths, headers, or other sensitive details to management users. Reverting stops future disclosure, but any information already displayed cannot be fully recovered.


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

@maiphucgiang
maiphucgiang merged commit dbaeef0 into maiphucgiang:main Oct 4, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants