fix(cc-config): one account is one profile, and rename it in place

Four defects, all from one design mistake: a profile's display name and its
filename were the same string.

- Saving the logged-in account made a SECOND profile of an account another
  profile already held. Both then reported themselves active, so there was
  nothing to switch between. `POST /profiles` now refuses `EDUPLICATE` and
  names the profile that already holds the account. Re-saving into the
  account's own profile stays allowed - that is the token-refresh path.
- Worse, an accidental duplicate could not be cleaned up: two profiles of one
  account are both active, and the delete guard refused any active profile, so
  neither was ever deletable. The guard now blocks only the LAST saved copy of
  the live account, which is the actual credential-loss condition it exists
  for. The trash button had to follow, or the server would allow what the UI
  hid.
- A name could not contain spaces or diacritics, because it had to be
  filename-safe. A profile now carries `name` (internal identifier, still the
  filename, still what every route takes) and `label` (what the user reads and
  edits: 1-60 free-form characters, control characters rejected, stored
  trimmed, unique case-insensitively). A label never touches a path, which is
  what makes "Tài khoản chính" a legal name.
- Naming happened up front in a "save current account as" field. That field is
  gone. The server derives the identifier, the save button lives on the live
  account's card and appears only while it is unsaved, and the label is edited
  by clicking it (Enter saves, Escape cancels).

Renaming rewrites the label in place and deliberately does NOT move `name`, so
it cannot half-fail and leave a profile unreachable. Profiles written before
labels existed report `label` equal to their `name`, so nothing needs migrating.

Adds `PATCH /api/cc-config/profiles`; `POST` now takes an optional `name`.

Verified: server 1431/1431 (11 new), client 459/459, tsc clean. Also exercised
end to end against a COPY of the real profile store on this machine: the
duplicate deleted, the last copy still refused, a Vietnamese label accepted,
and the stored access/refresh tokens and 0600 mode intact after a rename.
This commit is contained in:
2026-09-07 16:29:11 +07:00
parent 4bc77ded17
commit 11dcc04ac4
12 changed files with 560 additions and 102 deletions
+8 -7
View File
@@ -222,12 +222,9 @@
"warning": "Close your running Claude Code sessions before switching - a live session rewrites the config on exit and would put the old account back.",
"activeLabel": "Logged in now",
"noActive": "No Claude account is logged in on this machine.",
"unsavedActive": "This account is not saved as a profile yet - save it below, otherwise you cannot switch away without losing its credentials.",
"saveLabel": "Save the current account as a profile",
"namePlaceholder": "profile name (e.g. work)",
"saveButton": "Save",
"saveHint": "The dashboard cannot log in for you. To add another account: run `claude auth login` in a terminal, sign in, then come back and save it under a new name.",
"saveSuccess": "Saved the current account as profile \"{{name}}\".",
"unsavedActive": "This account is not saved as a profile yet - save it, otherwise you cannot switch away without losing its credentials.",
"saveButton": "Save this account as a profile",
"saveSuccess": "Saved the account you are logged in as. Click its name to rename it.",
"empty": "No profiles saved yet - save the account you are logged in as to get started.",
"activeBadge": "active",
"switchButton": "Switch to",
@@ -236,7 +233,11 @@
"deleteButton": "Delete profile",
"deleteSuccess": "Deleted profile \"{{name}}\".",
"savedAt": "Saved {{time}}",
"loginHint": "Add another account by logging in from a terminal first:"
"loginHint": "Add another account by logging in from a terminal first:",
"saving": "Saving…",
"renameLabel": "Profile name",
"renameHint": "Click to rename",
"renameSuccess": "Renamed to \"{{name}}\"."
},
"explain": {
"hooks": {
+8 -7
View File
@@ -217,12 +217,9 @@
"warning": "Hãy đóng các phiên Claude Code đang chạy trước khi đổi - phiên đang chạy sẽ ghi lại config khi thoát và đưa tài khoản cũ quay về.",
"activeLabel": "Đang đăng nhập",
"noActive": "Chưa có tài khoản Claude nào đăng nhập trên máy này.",
"unsavedActive": "Tài khoản này chưa được lưu thành profile - hãy lưu bên dưới, nếu không sẽ không thể đổi sang profile khác mà không mất credentials của nó.",
"saveLabel": "Lưu tài khoản hiện tại thành profile",
"namePlaceholder": "tên profile (vd: work)",
"saveButton": "Lưu",
"saveHint": "Dashboard không thể đăng nhập thay bạn. Để thêm tài khoản khác: chạy `claude auth login` trong terminal, đăng nhập, rồi quay lại đây lưu với tên mới.",
"saveSuccess": "Đã lưu tài khoản hiện tại thành profile \"{{name}}\".",
"unsavedActive": "Tài khoản này chưa được lưu thành profile - hãy lưu lại, nếu không sẽ không thể đổi sang profile khác mà không mất credentials của nó.",
"saveButton": "Lưu tài khoản này thành profile",
"saveSuccess": "Đã lưu tài khoản đang đăng nhập. Bấm vào tên của nó để đổi tên.",
"empty": "Chưa có profile nào - hãy lưu tài khoản đang đăng nhập để bắt đầu.",
"activeBadge": "đang dùng",
"switchButton": "Chuyển sang",
@@ -231,7 +228,11 @@
"deleteButton": "Xoá profile",
"deleteSuccess": "Đã xoá profile \"{{name}}\".",
"savedAt": "Lưu lúc {{time}}",
"loginHint": "Thêm tài khoản khác bằng cách đăng nhập từ terminal trước:"
"loginHint": "Thêm tài khoản khác bằng cách đăng nhập từ terminal trước:",
"saving": "Đang lưu…",
"renameLabel": "Tên profile",
"renameHint": "Bấm để đổi tên",
"renameSuccess": "Đã đổi tên thành \"{{name}}\"."
},
"explain": {
"hooks": {