diff --git a/CLAUDE.md b/CLAUDE.md index 50b2f263..d121ed61 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,6 +1,6 @@ # Claude Code Working Guide -_Last synced: v2.8.6 (2026-10-15) — Users page is now Administration (`/admin`: Users + Server tabs, `GET /api/host` staff-only, read-only per-user CPU/memory)._ A change that makes any statement in this file, `AGENTS.md`, or a README wrong — a moved path, a new or changed extension point, a retired rule — updates that file in the same change-set and bumps this marker. **Bump = replace the version/date and a ≤ 1-line summary; never append a changelog here** — details belong in the commit message and `docs/`, because this whole file is loaded into every session. +_Last synced: v2.8.7 (2026-10-15) — Settings → Git SSH keys are tabs per host kind; Artifacts preview only what the sandbox can show._ A change that makes any statement in this file, `AGENTS.md`, or a README wrong — a moved path, a new or changed extension point, a retired rule — updates that file in the same change-set and bumps this marker. **Bump = replace the version/date and a ≤ 1-line summary; never append a changelog here** — details belong in the commit message and `docs/`, because this whole file is loaded into every session. ## Project mission - Maintain a reliable local-first dashboard for Claude Code session monitoring. diff --git a/client/src/components/settings/GitIdentitySettings.tsx b/client/src/components/settings/GitIdentitySettings.tsx index c4d13a69..13aabe2f 100644 --- a/client/src/components/settings/GitIdentitySettings.tsx +++ b/client/src/components/settings/GitIdentitySettings.tsx @@ -13,7 +13,8 @@ * that same file, so a plain `ssh://git-codecommit…` URL works), and for * SEVERAL accounts (two AWS accounts, a second GitHub user) named keys made * beside the default ones plus a login per account NAME (`ssh:///…`), - * each tied to the one key it uses — and, for + * each tied to the one key it uses (the SSH part is split into tabs: one per + * kind of host, the extra keys, the other hosts) — and, for * a user with their own Linux account (S3), their own HTTPS * logins. The host account's HTTPS logins are the shared Git credentials * table above it, so this lists them only for a member. The private key and @@ -23,12 +24,23 @@ */ import { useCallback, useEffect, useState, type ReactNode } from "react"; import { useTranslation } from "react-i18next"; -import { Copy, KeyRound, Plus, RefreshCw, Trash2 } from "lucide-react"; +import { + Cloud, + Copy, + GitBranch, + KeyRound, + KeySquare, + Plus, + RefreshCw, + Server, + Trash2, +} from "lucide-react"; import { api, type GitIdentity } from "../../lib/api"; import { confirmDialog } from "../../lib/confirm"; import { copyText } from "../../lib/clipboard"; import { toast } from "../../lib/toast"; import { useAuth } from "../AuthGate"; +import { PageTabs } from "../PageTabs"; import { Switch } from "../Switch"; const INPUT = @@ -180,6 +192,8 @@ export function GitIdentitySettings() { const me = auth.mode === "server" ? auth.user : null; // null = not edited: the field shows the saved value, else the account's. const [author, setAuthor] = useState<{ name: string; email: string } | null>(null); + // null = not picked: opens on the first kind of host that has a key. + const [sshTab, setSshTab] = useState(null); const load = useCallback(async () => { try { @@ -378,6 +392,31 @@ export function GitIdentitySettings() { authorUnset || authorShown.name.trim() !== (id.author.name ?? "") || authorShown.email.trim() !== (id.author.email ?? ""); + // The SSH keys, one tab per kind of host — every section at once was a wall. + const sshTabs = [ + { id: "git", label: t("gitIdentity.tabGit"), Icon: GitBranch }, + { id: "aws", label: t("gitIdentity.awsTitle"), Icon: Cloud }, + ...(named.length > 0 || id.writable + ? [ + { + id: "more", + label: named.length + ? `${t("gitIdentity.tabMore")} (${named.length})` + : t("gitIdentity.tabMore"), + Icon: KeySquare, + }, + ] + : []), + ...(id.sshUsers !== undefined && (otherRows.length > 0 || id.writable) + ? [{ id: "other", label: t("gitIdentity.tabOther"), Icon: Server }] + : []), + ]; + const activeSshTab = + sshTab && sshTabs.some((x) => x.id === sshTab) + ? sshTab + : !keyOf("ed25519") && keyOf("rsa") + ? "aws" + : "git"; const saveAuthor = async () => { const ok = await run( () => @@ -452,43 +491,252 @@ export function GitIdentitySettings() {

{t("gitIdentity.sshIntro")}

- - {t("gitIdentity.gitHubStepAdd")} - - {t("gitIdentity.gitHubStepClone")} - git clone git@github.com:<org>/<repo>.git - - + {activeSshTab === "git" && ( + + {t("gitIdentity.gitHubStepAdd")} + + {t("gitIdentity.gitHubStepClone")} + git clone git@github.com:<org>/<repo>.git + + + )} - - {t("gitIdentity.awsStepUpload")} - - {t("gitIdentity.awsStepKeyId")} - {id.sshUsers === undefined ? null : ( - <> - {awsRows.map((r) => ( -
+ {activeSshTab === "aws" && ( + + {t("gitIdentity.awsStepUpload")} + + {t("gitIdentity.awsStepKeyId")} + {id.sshUsers === undefined ? null : ( + <> + {awsRows.map((r) => ( +
+ + {r.alias ? `${r.alias} · ` : ""} + {r.region} · {r.user} · {keyLabel(r.key)} + + {id.writable && ( + + )} +
+ ))} + {id.writable && ( +
{ + e.preventDefault(); + void saveAws(); + }} + > + setAws({ ...aws, keyId: e.target.value })} + className={`${INPUT} font-mono`} + /> + + setAws({ ...aws, alias: e.target.value })} + className={`${INPUT} font-mono`} + /> + {keySelect(aws.key, (key) => setAws({ ...aws, key }), "rsa")} + +
+ )} +
{t("gitIdentity.awsMany")}
+ + )} +
+ {awsRows.length > 0 && ( +
+ {t("gitIdentity.awsReady")} + {awsRows.map((r) => ( + + git clone ssh://{r.alias ?? r.host}/v1/repos/<repo> + + ))} +
+ )} +
+ )} + + {activeSshTab === "more" && ( +
+
+
+
+ {t("gitIdentity.moreKeysTitle")} +
+
{t("gitIdentity.moreKeysSub")}
+
+ {id.writable && !newKey && ( + + )} +
+ {newKey && ( +
{ + e.preventDefault(); + void makeNamed(); + }} + > + setNewKey({ ...newKey, name: e.target.value })} + className={`${INPUT} font-mono`} + /> + + + +
+ )} + {named.map((k) => ( +
+
+ + {k.name} + + + {k.type === "rsa" ? "RSA" : "ED25519"} + + + {k.fingerprint} + + + {id.writable && ( + + )} +
+