feat: v2.7.6 — edit users in one form, Settings → Account, display names, copy over plain HTTP

- Users page: one Edit user form (display name, email, role, active, a new or
  generated password, workspaces given or taken back, sign out everywhere),
  validated as a whole before anything is written (PATCH /api/users/:id,
  GET /api/users/:id); users.display_name/email added migration-safe.
- Settings → Account for every signed-in user: profile, own password (the
  current one required, this login kept), sign out other devices
  (/api/me/account, /api/me/password, /api/me/sign-out-others; login cookie
  only, never a pane token).
- Display names wherever a person is named (sidebar, workspace owner groups,
  Users page and audit log); owner_display_name / reader_display additive.
- Users page: admins add and remove Claude/Codex pool accounts.
- Folder browser: an admin with their own Linux account opens in their home;
  the "Server folder" tick opens the host's (never remembered).
- AI Proxy: "Config 9router" beside Add profile when 9router is installed,
  opened on the dashboard's own host (GET /api/ai-proxies/9router).
- Terminal: a tmux selection reaches the clipboard over plain HTTP too
  (writeClipboard's textarea fallback, focus handed back).
- Chat: a file the session touched outside the workspace previews
  (GET /api/sessions/:id/file).
- install-system: `!requiretty` only when visudo accepts it (sudo-rs on
  Ubuntu 25.10+ rejects the setting).
- openapi.yaml regenerated; docs (API, SERVER-MODE, LANES) updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-13 17:10:29 +07:00
parent 9c573f1e3c
commit 0c2f49876d
74 changed files with 5347 additions and 392 deletions
+2
View File
@@ -58,6 +58,8 @@
"browse.recent": "Recent",
"browse.selectFile": "Select file",
"browse.showHidden": "Show hidden",
"browse.hostFolder": "Server folder",
"browse.hostFolderHint": "Browse the server account's own folder ({{path}}). A workspace there runs as the shared server account, which every admin uses — not as your own.",
"browse.up": "Up one level (Backspace)",
"browse.workspaces": "Workspaces",
"browse.title": "Browse for a folder",
+33
View File
@@ -626,6 +626,7 @@
"title": "AI Proxy",
"description": "For example 9Router, OpenRouter or a self-hosted gateway. Save one here, then pick it for a session in the workspace prompt box.",
"add": "Add profile",
"config9router": "Config 9router",
"edit": "Edit",
"remove": "Remove",
"empty": "No proxy profile saved yet. Native runs against the vendor's own API.",
@@ -820,6 +821,10 @@
"data": "Data"
},
"section": {
"account": {
"title": "Account",
"description": "Your profile, password and sign-ins on this server."
},
"appearance": {
"title": "Appearance",
"description": "Language, colour mode and theme, and the corner pet."
@@ -1044,5 +1049,33 @@
"chooseFile": "Choose file",
"fileTooBig": "{{name}} is {{size}} characters — instructions hold at most {{max}}",
"appliesTo": "Applies to"
},
"account": {
"loadFailed": "Could not load your account: {{message}}",
"memberSince": "Member since",
"workspaces": "Workspaces",
"sessions": "Sessions",
"logins": "Active sign-ins",
"lastLogin": "Last sign-in",
"roleNote": "Your role and access are set by an admin on the Users page.",
"profile": "Profile",
"profileDescription": "How your name appears to others on this server.",
"save": "Save",
"profileSaved": "Profile saved",
"password": "Password",
"passwordDescription": "Changing it keeps this browser signed in and signs you out everywhere else.",
"currentPassword": "Current password",
"newPassword": "New password",
"confirmPassword": "Confirm new password",
"mismatch": "The two new passwords differ.",
"changePassword": "Change password",
"passwordChanged": "Password changed",
"sessionsTitle": "Sign-ins",
"sessionsDescription": "You are signed in on {{n}} browser(s), this one included.",
"signOutOthers": "Sign out other devices",
"signOutOthersConfirm": "Sign out every other device?",
"signOutOthersBody": "Every browser signed in to your account except this one is signed out.",
"signedOut_one": "Signed out {{count}} other device",
"signedOut_other": "Signed out {{count}} other devices"
}
}
+119 -3
View File
@@ -56,7 +56,8 @@
"disabled": "{{name}} disabled",
"enabled": "{{name}} enabled",
"password": "{{name}}'s password reset",
"workspace": "Workspace given to {{name}}"
"workspace": "Workspace given to {{name}}",
"updated": "{{name}} updated"
},
"errors": {
"EREAUTH": "Your password is wrong.",
@@ -64,12 +65,127 @@
"ESELF": "You cannot change your own role or disable yourself.",
"EEXISTS": "That username is taken.",
"EBADUSER": "Username: letters, digits, '.', '_' or '-' (up to 64).",
"EBADPASSWORD": "Password cannot be empty."
"EBADPASSWORD": "Password cannot be empty.",
"EBADEMAIL": "That email address does not look valid.",
"EBADFIELD": "One of the values is not valid.",
"ENOLANE": "That workspace no longer exists.",
"ENOTOWNED": "That workspace is not this user's any more.",
"EOWNERFOLDER": "That workspace's folder belongs to another user's account.",
"EBADROLE": "Unknown role.",
"ELOCALMODE": "Accounts are managed in server mode only."
},
"readOnly": "Analysts can view accounts but not change them.",
"auditTitle": "Audit log",
"auditDescription": "Account changes made here, and each time an admin or analyst opened a session a developer owns.",
"auditEmpty": "Nothing recorded yet.",
"activeCount": "{{n}} active",
"cost30d": "Cost · 30 days"
"cost30d": "Cost · 30 days",
"displayName": "Display name",
"displayNameHint": "Shown beside the username. Optional.",
"email": "Email",
"edit": {
"button": "Edit user",
"title": "Edit {{name}}",
"created": "Created",
"lastLogin": "Last sign-in",
"never": "Never",
"logins": "Active sign-ins",
"profile": "Profile",
"access": "Access",
"status": "Status",
"active": "Active",
"statusHint": "A disabled user cannot sign in; their workspaces and history stay.",
"selfRole": "You cannot change your own role.",
"selfDisable": "You cannot disable yourself.",
"password": "Password",
"passwordHint": "Leave blank to keep the current one. A new one signs them out everywhere.",
"show": "Show password",
"hide": "Hide password",
"copy": "Copy password",
"generate": "Generate",
"loading": "Loading…",
"noOwned": "Owns no workspace.",
"undo": "Undo",
"removeWorkspace": "Remove {{name}}",
"removeWorkspaceHint": "The workspace is left with no owner",
"addWorkspace": "Add workspace",
"chooseWorkspace": "Choose a workspace…",
"sessions": "Sign-ins",
"signOut": "Sign out everywhere",
"signOutHint": "Ends this user's {{n}} active sign-in(s); they sign in again with their password.",
"noChanges": "No changes yet."
},
"pool": {
"title": "Account pool",
"description": "Claude and Codex accounts every user's new sessions run on — the least-used one is picked, and a login is written into the user's home by the server, so nobody signs in per user.",
"refresh": "Re-read quotas",
"add": "Add account",
"empty": "No accounts yet. Until you add one, each user's session needs that user's own sign-in.",
"account": "Account",
"tool": "Tool",
"livePanes": "Live",
"quota": "Quota",
"spend": "5 hours",
"spend7d": "{{cost}} in 7 days",
"added": "added {{at}}",
"expired": "expired",
"full": "full",
"kind": {
"token": "Claude · setup-token",
"login": "Claude · login",
"apikey": "Codex · API key",
"chatgpt": "Codex · ChatGPT"
},
"name": "Name",
"nameHint": "Any label you will recognise: letters, digits, '.', '_' or '-'",
"remove": "Remove {{name}}",
"removeHint": "New sessions stop using this account. Sessions already running on it keep running.",
"removeButton": "Remove",
"done": {
"added": "{{name}} added to the pool",
"removed": "{{name}} removed from the pool"
},
"errors": {
"EREAUTH": "Your password is wrong.",
"ETOOMANY": "Too many wrong passwords. Try again in 15 minutes.",
"EBADNAME": "Name: 1–64 letters, digits, '.', '_' or '-'.",
"EBADTOKEN": "That is not a valid token for this tool.",
"EEXISTS": "An account with that name is already in the pool.",
"ENOACCOUNT": "That account is no longer in the pool.",
"ELOCALMODE": "In local mode the pool is changed from the host's shell (agenthub accounts).",
"EACTIVE": "This server's Claude Code keeps that login in the macOS Keychain. Switch it to another profile first.",
"ENOTFOUND": "That saved login no longer exists.",
"ESTALE": "That login was already refreshed elsewhere. Log in again and save it anew.",
"EUPSTREAM": "Anthropic could not be reached. Try again."
},
"source": "From",
"sources": {
"profile": "A saved login (Switch account profiles)",
"credentials": "Paste .credentials.json / Keychain",
"token": "claude setup-token"
},
"sourceHint": {
"profile": "The login is moved into the pool: the profile leaves the switcher, and the server writes it into each user's home when their session starts.",
"credentials": "From ~/.claude/.credentials.json, or on a Mac `security find-generic-password -s \"Claude Code-credentials\" -w`. Do not keep using that login where you copied it from — two copies log each other out.",
"token": "A one-year token from `claude setup-token`, given to every session as an environment variable."
},
"profile": "Profile",
"profileHint": "Saved in Settings → Claude config → Switch account. The one this server's own Claude Code is logged into can be moved too: the server keeps running on it and refreshes it for everyone.",
"chooseProfile": "Choose a saved login…",
"noProfiles": "No saved logins — save one in Settings → Claude config → Switch account",
"loadingProfiles": "Loading…",
"activeHere": "in use on this server — keeps working",
"nameFromProfile": "Leave blank to use the profile's name",
"secret": {
"credentials": "Credentials JSON",
"token": "Token",
"codex": "API key or auth.json"
},
"secretHint": {
"credentials": "The whole file: {\"claudeAiOauth\": {…}}",
"token": "The sk-ant-… that `claude setup-token` printed.",
"codex": "An OpenAI API key (sk-…), or the whole ~/.codex/auth.json after `codex login` with a ChatGPT account."
},
"refreshFailed": "refresh failed"
}
}
+2
View File
@@ -58,6 +58,8 @@
"browse.recent": "Gần đây",
"browse.selectFile": "Chọn tệp",
"browse.showHidden": "Hiện mục ẩn",
"browse.hostFolder": "Thư mục máy chủ",
"browse.hostFolderHint": "Duyệt thư mục của tài khoản máy chủ ({{path}}). Workspace ở đó chạy bằng tài khoản máy chủ dùng chung cho mọi admin — không phải tài khoản riêng của bạn.",
"browse.up": "Lên một cấp (Backspace)",
"browse.workspaces": "Workspace",
"browse.title": "Duyệt chọn thư mục",
+33
View File
@@ -626,6 +626,7 @@
"title": "AI Proxy",
"description": "Ví dụ 9Router, OpenRouter hoặc gateway tự dựng. Lưu ở đây rồi chọn cho từng session ở ô nhập prompt của workspace.",
"add": "Thêm profile",
"config9router": "Config 9router",
"edit": "Sửa",
"remove": "Xoá",
"empty": "Chưa lưu profile proxy nào. Native là chạy thẳng vào API của hãng.",
@@ -820,6 +821,10 @@
"data": "Dữ liệu"
},
"section": {
"account": {
"title": "Tài khoản",
"description": "Hồ sơ, mật khẩu và các phiên đăng nhập của bạn trên server này."
},
"appearance": {
"title": "Giao diện",
"description": "Ngôn ngữ, chế độ màu và theme, và linh vật ở góc màn hình."
@@ -1044,5 +1049,33 @@
"chooseFile": "Chọn file",
"fileTooBig": "{{name}} dài {{size}} ký tự — hướng dẫn tối đa {{max}}",
"appliesTo": "Áp dụng cho"
},
"account": {
"loadFailed": "Không tải được tài khoản: {{message}}",
"memberSince": "Tham gia từ",
"workspaces": "Workspace",
"sessions": "Session",
"logins": "Phiên đăng nhập",
"lastLogin": "Đăng nhập gần nhất",
"roleNote": "Vai trò và quyền truy cập của bạn do admin đặt ở trang Người dùng.",
"profile": "Hồ sơ",
"profileDescription": "Tên của bạn hiển thị với người khác trên server này.",
"save": "Lưu",
"profileSaved": "Đã lưu hồ sơ",
"password": "Mật khẩu",
"passwordDescription": "Đổi mật khẩu giữ trình duyệt này đăng nhập và đăng xuất bạn ở mọi nơi khác.",
"currentPassword": "Mật khẩu hiện tại",
"newPassword": "Mật khẩu mới",
"confirmPassword": "Nhập lại mật khẩu mới",
"mismatch": "Hai mật khẩu mới không khớp.",
"changePassword": "Đổi mật khẩu",
"passwordChanged": "Đã đổi mật khẩu",
"sessionsTitle": "Phiên đăng nhập",
"sessionsDescription": "Bạn đang đăng nhập trên {{n}} trình duyệt, tính cả trình duyệt này.",
"signOutOthers": "Đăng xuất các thiết bị khác",
"signOutOthersConfirm": "Đăng xuất mọi thiết bị khác?",
"signOutOthersBody": "Mọi trình duyệt đang đăng nhập tài khoản của bạn, trừ trình duyệt này, sẽ bị đăng xuất.",
"signedOut_one": "Đã đăng xuất {{count}} thiết bị khác",
"signedOut_other": "Đã đăng xuất {{count}} thiết bị khác"
}
}
+119 -3
View File
@@ -56,7 +56,8 @@
"disabled": "Đã khoá {{name}}",
"enabled": "Đã mở khoá {{name}}",
"password": "Đã đặt lại mật khẩu cho {{name}}",
"workspace": "Đã giao workspace cho {{name}}"
"workspace": "Đã giao workspace cho {{name}}",
"updated": "Đã cập nhật {{name}}"
},
"errors": {
"EREAUTH": "Mật khẩu của bạn không đúng.",
@@ -64,12 +65,127 @@
"ESELF": "Bạn không thể tự đổi vai trò hay tự khoá chính mình.",
"EEXISTS": "Tên đăng nhập này đã có người dùng.",
"EBADUSER": "Tên đăng nhập: chữ, số, '.', '_' hoặc '-' (tối đa 64 ký tự).",
"EBADPASSWORD": "Mật khẩu không được để trống."
"EBADPASSWORD": "Mật khẩu không được để trống.",
"EBADEMAIL": "Địa chỉ email không hợp lệ.",
"EBADFIELD": "Có một giá trị không hợp lệ.",
"ENOLANE": "Workspace đó không còn tồn tại.",
"ENOTOWNED": "Workspace đó không còn thuộc người này.",
"EOWNERFOLDER": "Thư mục của workspace đó thuộc tài khoản của người dùng khác.",
"EBADROLE": "Vai trò không hợp lệ.",
"ELOCALMODE": "Chỉ quản lý tài khoản được ở chế độ server."
},
"readOnly": "Analyst chỉ xem được tài khoản, không thay đổi được.",
"auditTitle": "Nhật ký audit",
"auditDescription": "Các thay đổi tài khoản thực hiện ở đây, và mỗi lần admin hoặc analyst mở phiên của một developer.",
"auditEmpty": "Chưa ghi nhận gì.",
"activeCount": "{{n}} đang hoạt động",
"cost30d": "Chi phí · 30 ngày"
"cost30d": "Chi phí · 30 ngày",
"displayName": "Tên hiển thị",
"displayNameHint": "Hiện cạnh tên đăng nhập. Không bắt buộc.",
"email": "Email",
"edit": {
"button": "Sửa người dùng",
"title": "Sửa {{name}}",
"created": "Ngày tạo",
"lastLogin": "Đăng nhập gần nhất",
"never": "Chưa bao giờ",
"logins": "Phiên đăng nhập",
"profile": "Hồ sơ",
"access": "Quyền truy cập",
"status": "Trạng thái",
"active": "Đang hoạt động",
"statusHint": "Người dùng bị vô hiệu hoá không thể đăng nhập; workspace và lịch sử vẫn được giữ.",
"selfRole": "Bạn không thể đổi vai trò của chính mình.",
"selfDisable": "Bạn không thể tự vô hiệu hoá mình.",
"password": "Mật khẩu",
"passwordHint": "Để trống để giữ mật khẩu hiện tại. Mật khẩu mới sẽ đăng xuất họ ở mọi nơi.",
"show": "Hiện mật khẩu",
"hide": "Ẩn mật khẩu",
"copy": "Sao chép mật khẩu",
"generate": "Tạo ngẫu nhiên",
"loading": "Đang tải…",
"noOwned": "Chưa sở hữu workspace nào.",
"undo": "Hoàn tác",
"removeWorkspace": "Bỏ {{name}}",
"removeWorkspaceHint": "Workspace sẽ không còn chủ sở hữu",
"addWorkspace": "Thêm workspace",
"chooseWorkspace": "Chọn một workspace…",
"sessions": "Phiên đăng nhập",
"signOut": "Đăng xuất ở mọi nơi",
"signOutHint": "Kết thúc {{n}} phiên đăng nhập đang hoạt động của người này; họ đăng nhập lại bằng mật khẩu.",
"noChanges": "Chưa có thay đổi."
},
"pool": {
"title": "Pool tài khoản",
"description": "Các tài khoản Claude và Codex mà phiên mới của mọi user chạy trên đó — tự chọn tài khoản ít tải nhất, server ghi login vào home của user, không ai phải đăng nhập riêng.",
"refresh": "Đọc lại quota",
"add": "Thêm tài khoản",
"empty": "Chưa có tài khoản nào. Khi chưa thêm, phiên của mỗi người dùng cần chính người đó đăng nhập.",
"account": "Tài khoản",
"tool": "Công cụ",
"livePanes": "Đang chạy",
"quota": "Quota",
"spend": "5 giờ",
"spend7d": "{{cost}} trong 7 ngày",
"added": "thêm lúc {{at}}",
"expired": "hết hạn",
"full": "đầy",
"kind": {
"token": "Claude · setup-token",
"login": "Claude · login",
"apikey": "Codex · API key",
"chatgpt": "Codex · ChatGPT"
},
"name": "Tên",
"nameHint": "Nhãn tuỳ ý để bạn nhận ra: chữ, số, '.', '_' hoặc '-'",
"remove": "Xoá {{name}}",
"removeHint": "Phiên mới sẽ không dùng tài khoản này nữa. Các phiên đang chạy trên nó vẫn chạy tiếp.",
"removeButton": "Xoá",
"done": {
"added": "Đã thêm {{name}} vào pool",
"removed": "Đã xoá {{name}} khỏi pool"
},
"errors": {
"EREAUTH": "Mật khẩu của bạn không đúng.",
"ETOOMANY": "Nhập sai mật khẩu quá nhiều lần. Thử lại sau 15 phút.",
"EBADNAME": "Tên: 1–64 ký tự chữ, số, '.', '_' hoặc '-'.",
"EBADTOKEN": "Token không hợp lệ cho công cụ này.",
"EEXISTS": "Đã có tài khoản trùng tên trong pool.",
"ENOACCOUNT": "Tài khoản đó không còn trong pool.",
"ELOCALMODE": "Ở chế độ local, pool được thay đổi từ shell của máy (agenthub accounts).",
"EACTIVE": "Claude Code của server giữ login này trong macOS Keychain. Hãy chuyển nó sang profile khác trước.",
"ENOTFOUND": "Login đã lưu đó không còn nữa.",
"ESTALE": "Login này đã được làm mới ở nơi khác. Đăng nhập lại và lưu lại.",
"EUPSTREAM": "Không kết nối được Anthropic. Thử lại."
},
"source": "Nguồn",
"sources": {
"profile": "Login đã lưu (profile Switch account)",
"credentials": "Dán .credentials.json / Keychain",
"token": "claude setup-token"
},
"sourceHint": {
"profile": "Login được chuyển vào pool: profile rời khỏi trình đổi tài khoản, và server ghi nó vào home của từng user khi phiên của họ bắt đầu.",
"credentials": "Lấy từ ~/.claude/.credentials.json, hoặc trên Mac: `security find-generic-password -s \"Claude Code-credentials\" -w`. Đừng dùng tiếp login đó ở nơi đã copy — hai bản sẽ làm nhau bị đăng xuất.",
"token": "Token 1 năm từ `claude setup-token`, đưa vào mọi phiên qua biến môi trường."
},
"profile": "Profile",
"profileHint": "Lưu ở Settings → Claude config → Switch account. Profile mà Claude Code của server đang dùng cũng chuyển được: server vẫn chạy trên nó và tự làm mới cho mọi người.",
"chooseProfile": "Chọn login đã lưu…",
"noProfiles": "Chưa có login nào — lưu ở Settings → Claude config → Switch account",
"loadingProfiles": "Đang tải…",
"activeHere": "server đang dùng — vẫn chạy tiếp",
"nameFromProfile": "Để trống để dùng tên profile",
"secret": {
"credentials": "Credentials JSON",
"token": "Token",
"codex": "API key hoặc auth.json"
},
"secretHint": {
"credentials": "Toàn bộ file: {\"claudeAiOauth\": {…}}",
"token": "Chuỗi sk-ant-… mà `claude setup-token` in ra.",
"codex": "OpenAI API key (sk-…), hoặc toàn bộ ~/.codex/auth.json sau khi `codex login` bằng tài khoản ChatGPT."
},
"refreshFailed": "làm mới lỗi"
}
}