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"
}
}