feat(pool): an admin's switch moves a user's Claude login at once
PUT /api/accounts/pins (and agenthub accounts pin) now writes a pinned Claude login into the user's home right away (claude-pool.switchHome): every home's newer pair is copied back first, the member's own login is never written over, and no pane is restarted — running sessions take the new pair at their next credentials read. A live home stays on the login it holds, so unpinning does not flip it back. The Users page calls it "Switch a user's account" and says what happened.
This commit is contained in:
@@ -18,8 +18,10 @@
|
||||
* 3. least $ spent in the last 5 hours, then in the last 7 days — the
|
||||
* sessions its runs (`dashboard_runs.account`) held, over the hours they
|
||||
* held them (pool-usage.js).
|
||||
* An admin may pin a user to one account (account-pins.js): that user's
|
||||
* panes skip the ranking whenever they may use it.
|
||||
* An admin may switch a user to one account (account-pins.js): that user's
|
||||
* panes skip the ranking whenever they may use it, and a login is written
|
||||
* into their home at once (`switchHome`) — running panes take it at their
|
||||
* next credentials read, without a restart.
|
||||
* The quota is read with the account's own token in the background, at most
|
||||
* every `QUOTA_TTL_MS`; a token the usage endpoint refuses simply has no
|
||||
* reading, and ranking falls back to spend.
|
||||
@@ -335,6 +337,40 @@ async function refreshLogins() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An admin's switch (account-pins.js): put pool login `name` into `member`'s
|
||||
* home NOW, over the pool login it holds — never over a login the member made
|
||||
* themselves. Every home's newer pair is copied back first (refreshLogins),
|
||||
* so the login leaving the home keeps its only live refresh token. Running
|
||||
* panes are not restarted: Claude Code takes the new pair when it next
|
||||
* re-reads its credentials (before a refresh), new panes at once. Answers
|
||||
* `switched`, `kept` (already there), `own-login` (left alone), `expired`, or
|
||||
* `next-session` (a setup-token, or no member home: it goes in at spawn).
|
||||
*/
|
||||
async function switchHome(member, name) {
|
||||
const first = readAccount(validName(name));
|
||||
if (!first) throw err("ENOACCOUNT", `no account ${name} in the pool`);
|
||||
if (first.kind !== "login" || !member || member.host) return "next-session";
|
||||
await refreshLogins();
|
||||
const a = readAccount(first.name);
|
||||
if (!a || expired(a)) return "expired";
|
||||
const disk = homeCreds(member);
|
||||
if (disk && ownsHome(a, member.name, disk)) return "kept";
|
||||
const holder = disk && allAccounts().find((x) => ownsHome(x, member.name, disk));
|
||||
if (disk && !holder) return "own-login";
|
||||
if (holder) {
|
||||
const placed = { ...holder.placed };
|
||||
delete placed[member.name];
|
||||
save({ ...holder, placed });
|
||||
}
|
||||
save(writeLogin(a, member));
|
||||
runAs().markClaudeOnboarded(member, {
|
||||
...(a.account ? { oauthAccount: a.account } : {}),
|
||||
...(a.userID ? { userID: a.userID } : {}),
|
||||
});
|
||||
return "switched";
|
||||
}
|
||||
|
||||
// ── Quota, read in the background ──────────────────────────────────────────
|
||||
|
||||
/** name → { at, fiveHour?, sevenDay?, error? } */
|
||||
@@ -461,7 +497,10 @@ function pick(member, ownerId = null) {
|
||||
} else {
|
||||
// One .credentials.json per home: while a pane of theirs runs on a
|
||||
// pool login, the next pane stays on that one.
|
||||
live = usable.find((a) => a.kind === "login" && liveInHome(a.name, member)) || null;
|
||||
// The login in the home is the one they run on — after an admin's
|
||||
// switch (switchHome) it is no longer the one those panes started on.
|
||||
const liveLogin = usable.find((a) => a.kind === "login" && liveInHome(a.name, member));
|
||||
live = liveLogin ? placed || liveLogin : null;
|
||||
}
|
||||
}
|
||||
// A pinned token goes in by env and wins outright; a pinned login waits
|
||||
@@ -550,6 +589,7 @@ module.exports = {
|
||||
poolStatus,
|
||||
pickAccount,
|
||||
forSpawn,
|
||||
switchHome,
|
||||
refreshQuotas,
|
||||
refreshLogins,
|
||||
__read: readAccount,
|
||||
|
||||
Reference in New Issue
Block a user