feat(pool): one board of pools with split/merge by drag; plan names from tier and seat

- The Users page account pool is one board: a column per pool holding its
  Claude Code, Codex and Cursor accounts (plan, quota, live panes) and its
  members. Accounts sit in one pool; drag moves them. Drop on New pool to
  split one off; drag a pool's only account into another pool to merge
  (members follow, the emptied pool is deleted on Save). One password Save.
- Claude plan label (planLabel in cc-profiles.js) reads rateLimitTier and
  seatTier: Max 5x/20x, Team Standard/Premium — subscriptionType alone said
  'pro' after an upgrade and 'team' for both seats. Shown on profiles, the
  status bar quota badge and pool cards; Codex pool cards show the ChatGPT plan.
This commit is contained in:
2026-10-28 11:47:19 +07:00
parent 9d07f64154
commit 26aabd6660
18 changed files with 1638 additions and 642 deletions
+18
View File
@@ -444,3 +444,21 @@ describe("cc-profiles — identity and token disagree", () => {
assert.throws(() => profiles.saveProfile(undefined, { owner }), { code: "EMISMATCH" });
});
});
it("planLabel reads the plan from the rate-limit tier and seat, not subscriptionType alone", () => {
const { planLabel } = profiles;
// An upgrade keeps `subscriptionType: "pro"` from sign-in; the tier is the truth.
assert.strictEqual(planLabel({ subscriptionType: "pro", rateLimitTier: "default_claude_max_5x" }, null), "Max 5x");
assert.strictEqual(planLabel({ subscriptionType: "max", rateLimitTier: "default_claude_max_20x" }, null), "Max 20x");
assert.strictEqual(planLabel({ subscriptionType: "pro" }, null), "Pro");
const team = { subscriptionType: "team", rateLimitTier: "default_raven" };
assert.strictEqual(planLabel(team, { seatTier: "team_standard" }), "Team Standard");
assert.strictEqual(planLabel(team, { seatTier: "team_premium" }), "Team Premium");
// `team_tier_1` names no kind; a Max-sized limit is what a premium seat gives.
assert.strictEqual(
planLabel(team, { seatTier: "team_tier_1", userRateLimitTier: "default_claude_max_5x" }),
"Team Premium"
);
assert.strictEqual(planLabel(team, null), "Team");
assert.strictEqual(planLabel(null, null), null);
});
+1 -1
View File
@@ -222,7 +222,7 @@ test("the live row is the account its token belongs to, not the one .claude.json
assert.strictEqual(live.label, "Victor");
assert.strictEqual(live.account, "victor@x.y");
assert.strictEqual(live.ownerUuid, undefined);
assert.strictEqual(live.plan, "max");
assert.strictEqual(live.plan, "Max");
assert.deepStrictEqual(
claude.filter((t) => !t.active).map((t) => t.label),
["Thắng"]