feat: persist rotation state across restarts, fix dashboard alignment
- Save currentIndex + rateLimits to JSON file in DATA_DIR - Restore on startup so rotation survives service restarts - Replace emoji indicators with text (*/!) for monospace alignment - Dynamic column width based on longest name - Show Codex accounts with plan type on dashboard
This commit is contained in:
@@ -10,9 +10,15 @@
|
||||
* All exhausted: fall through to provider fallback (Kimi etc.)
|
||||
*/
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
import { DATA_DIR } from './config.js';
|
||||
import { readEnvFile } from './env.js';
|
||||
import { logger } from './logger.js';
|
||||
|
||||
const STATE_FILE = path.join(DATA_DIR, 'token-rotation-state.json');
|
||||
|
||||
interface TokenState {
|
||||
token: string;
|
||||
rateLimitedUntil: number | null;
|
||||
@@ -31,11 +37,9 @@ export function initTokenRotation(): void {
|
||||
'CLAUDE_CODE_OAUTH_TOKEN',
|
||||
]);
|
||||
const multi =
|
||||
process.env.CLAUDE_CODE_OAUTH_TOKENS ||
|
||||
envFile.CLAUDE_CODE_OAUTH_TOKENS;
|
||||
process.env.CLAUDE_CODE_OAUTH_TOKENS || envFile.CLAUDE_CODE_OAUTH_TOKENS;
|
||||
const single =
|
||||
process.env.CLAUDE_CODE_OAUTH_TOKEN ||
|
||||
envFile.CLAUDE_CODE_OAUTH_TOKEN;
|
||||
process.env.CLAUDE_CODE_OAUTH_TOKEN || envFile.CLAUDE_CODE_OAUTH_TOKEN;
|
||||
|
||||
const raw = multi
|
||||
? multi
|
||||
@@ -51,13 +55,44 @@ export function initTokenRotation(): void {
|
||||
}
|
||||
|
||||
if (tokens.length > 1) {
|
||||
loadState();
|
||||
logger.info(
|
||||
{ count: tokens.length },
|
||||
{ count: tokens.length, activeIndex: currentIndex },
|
||||
`Token rotation initialized with ${tokens.length} tokens`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function saveState(): void {
|
||||
try {
|
||||
const state = {
|
||||
currentIndex,
|
||||
rateLimits: tokens.map((t) => t.rateLimitedUntil),
|
||||
};
|
||||
fs.writeFileSync(STATE_FILE, JSON.stringify(state));
|
||||
} catch { /* best effort */ }
|
||||
}
|
||||
|
||||
function loadState(): void {
|
||||
try {
|
||||
if (!fs.existsSync(STATE_FILE)) return;
|
||||
const state = JSON.parse(fs.readFileSync(STATE_FILE, 'utf-8'));
|
||||
const now = Date.now();
|
||||
if (typeof state.currentIndex === 'number' && state.currentIndex < tokens.length) {
|
||||
currentIndex = state.currentIndex;
|
||||
}
|
||||
if (Array.isArray(state.rateLimits)) {
|
||||
for (let i = 0; i < Math.min(state.rateLimits.length, tokens.length); i++) {
|
||||
const until = state.rateLimits[i];
|
||||
if (typeof until === 'number' && until > now) {
|
||||
tokens[i].rateLimitedUntil = until;
|
||||
}
|
||||
}
|
||||
}
|
||||
logger.info({ currentIndex, tokenCount: tokens.length }, 'Token rotation state restored');
|
||||
} catch { /* start fresh */ }
|
||||
}
|
||||
|
||||
/** Get the current active token. */
|
||||
export function getCurrentToken(): string | undefined {
|
||||
if (tokens.length === 0) return process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
||||
@@ -86,6 +121,7 @@ export function rotateToken(): boolean {
|
||||
{ tokenIndex: currentIndex, totalTokens: tokens.length },
|
||||
`Rotated to token #${currentIndex + 1}/${tokens.length}`,
|
||||
);
|
||||
saveState();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -103,6 +139,7 @@ export function markTokenHealthy(): void {
|
||||
const state = tokens[currentIndex];
|
||||
if (state?.rateLimitedUntil) {
|
||||
state.rateLimitedUntil = null;
|
||||
saveState();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user