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
+7 -5
View File
@@ -736,10 +736,12 @@ function inferredDisplayStage(lane) {
return lane.detected_stage;
}
/** The owner's username — the staff sidebar groups workspaces by it. */
function ownerName(id) {
if (id == null) return null;
return db.prepare("SELECT username FROM users WHERE id = ?").get(id)?.username ?? null;
/** The owner's username and display name — the staff sidebar groups
* workspaces by owner and shows the display name when one is set. */
function ownerNames(id) {
if (id == null) return { owner_name: null, owner_display_name: null };
const u = db.prepare("SELECT username, display_name FROM users WHERE id = ?").get(id);
return { owner_name: u?.username ?? null, owner_display_name: u?.display_name ?? null };
}
function lanePayload(lane, ageSec = null) {
@@ -755,7 +757,7 @@ function lanePayload(lane, ageSec = null) {
delete row.instructions;
return {
...row,
owner_name: ownerName(lane.owner_id),
...ownerNames(lane.owner_id),
pipeline_name: pipeline.name,
pipeline_skill: pipeline.skill || "",
pipeline_nodes: