Codex accounts: 6h auto-refresh, manual refresh, manual switch (#48)

Auto refresh
- New refreshCodexAccount(index) calls https://auth.openai.com/oauth/token
  with grant_type=refresh_token and persists rotated tokens back to
  ~/.codex-accounts/{N}/auth.json. JWT's subscription_active_until
  reflects the latest plan state OpenAI hands back.
- startCodexAccountRefreshLoop() runs every 6h (first run delayed 60s
  after boot to keep startup snappy). Hooked into main()/shutdown()
  alongside the existing claude token refresh loop.

Manual controls (Settings → 계정 → Codex)
- "갱신" button per row: forces an immediate refresh for that account.
- "전체 갱신" button: refreshes all codex accounts in sequence.
- "전환" button: writes data/codex-rotation-state.json so the next codex
  spawn picks the chosen account. Active account marked with a green-
  bordered card and "사용중" badge.

Endpoints
- POST /api/settings/accounts/codex/{i}/refresh
- POST /api/settings/accounts/codex/refresh-all
- PUT  /api/settings/accounts/codex/current  { index }
- GET  /api/settings/accounts now also returns codexCurrentIndex.

Why
JWTs cache subscription state at issue-time. When a Pro plan lapses or
a user upgrades/downgrades, the dashboard kept showing stale data until
the user logged in again. Periodic refresh + explicit "갱신" button
keeps the displayed state honest, and the manual switch unblocks the
user when rotation lands on a known-bad account.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Eyejoker
2026-04-27 22:23:06 +09:00
committed by GitHub
parent 2f7e09e194
commit c2a6889567
7 changed files with 437 additions and 13 deletions

View File

@@ -897,6 +897,13 @@ button:disabled {
gap: 4px;
}
.settings-account-group-head {
display: flex;
gap: 8px;
align-items: center;
justify-content: space-between;
}
.settings-account-row {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
@@ -908,6 +915,38 @@ button:disabled {
background: rgba(255, 255, 255, 0.02);
}
.settings-account-row.is-active-account {
border-color: rgba(80, 180, 130, 0.45);
background: rgba(80, 180, 130, 0.06);
}
.settings-account-actions {
display: flex;
gap: 6px;
align-items: center;
}
.settings-secondary {
min-height: 28px;
padding: 0 10px;
border: 1px solid var(--panel-border-strong);
border-radius: 6px;
background: rgba(255, 255, 255, 0.04);
color: var(--bg-ink);
font-size: 11px;
font-weight: 700;
cursor: pointer;
}
.settings-secondary:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.settings-secondary:hover:not(:disabled) {
background: rgba(255, 255, 255, 0.08);
}
.settings-account-main {
display: grid;
grid-template-columns: 28px minmax(0, 1fr) 60px 180px;