diff --git a/apps/dashboard/src/App.tsx b/apps/dashboard/src/App.tsx index 3a71d89..162485c 100644 --- a/apps/dashboard/src/App.tsx +++ b/apps/dashboard/src/App.tsx @@ -1234,6 +1234,34 @@ function ModelSettings({ onRestartStack }: { onRestartStack: () => void }) { ); } +function formatExpiry( + iso: string | null, +): { label: string; cls: string } | null { + if (!iso) return null; + const dt = new Date(iso); + if (Number.isNaN(dt.getTime())) return null; + const days = (dt.getTime() - Date.now()) / 86400000; + const dateStr = dt.toLocaleDateString('ko-KR', { + year: '2-digit', + month: '2-digit', + day: '2-digit', + }); + if (days < 0) { + const ago = Math.ceil(-days); + return { label: `${dateStr} 만료 (${ago}일 전)`, cls: 'is-expired' }; + } + if (days < 7) { + return { + label: `${dateStr}까지 (${Math.floor(days)}일)`, + cls: 'is-soon', + }; + } + return { + label: `${dateStr}까지 (${Math.floor(days)}일)`, + cls: 'is-active', + }; +} + function AccountSettings({ onRestartStack }: { onRestartStack: () => void }) { const [data, setData] = useState<{ claude: ClaudeAccountSummary[]; @@ -1309,29 +1337,39 @@ function AccountSettings({ onRestartStack }: { onRestartStack: () => void }) {

계정 없음

) : ( )}
@@ -1359,29 +1397,51 @@ function AccountSettings({ onRestartStack }: { onRestartStack: () => void }) {

계정 없음

) : ( )}

diff --git a/apps/dashboard/src/api.ts b/apps/dashboard/src/api.ts index 01cf643..a62416d 100644 --- a/apps/dashboard/src/api.ts +++ b/apps/dashboard/src/api.ts @@ -355,8 +355,10 @@ export interface ClaudeAccountSummary { export interface CodexAccountSummary { index: number; accountId: string | null; + email: string | null; planType: string | null; subscriptionUntil: string | null; + subscriptionLastChecked: string | null; exists: boolean; } diff --git a/apps/dashboard/src/styles.css b/apps/dashboard/src/styles.css index 9f726b3..4824dd1 100644 --- a/apps/dashboard/src/styles.css +++ b/apps/dashboard/src/styles.css @@ -864,21 +864,74 @@ button:disabled { .settings-account-row { display: grid; - grid-template-columns: 40px minmax(0, 1fr) auto; + grid-template-columns: minmax(0, 1fr) auto; gap: 10px; align-items: center; - padding: 6px 8px; + padding: 8px 10px; border: 1px solid var(--panel-border); - border-radius: 6px; + border-radius: 8px; background: rgba(255, 255, 255, 0.02); } +.settings-account-main { + display: flex; + flex-wrap: wrap; + gap: 6px 10px; + align-items: center; + min-width: 0; +} + .settings-account-tag { + flex: none; font-family: 'JetBrains Mono', ui-monospace, monospace; font-size: 11px; color: var(--muted); } +.settings-account-email { + font-size: 13px; + font-weight: 600; + color: var(--bg-ink); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 100%; +} + +.settings-account-plan { + font-size: 11px; + font-weight: 700; + letter-spacing: 0.04em; + text-transform: uppercase; + padding: 2px 7px; + border-radius: 999px; + background: rgba(255, 255, 255, 0.06); + color: var(--muted); +} + +.settings-account-badge { + font-size: 11px; + font-weight: 600; + padding: 2px 8px; + border-radius: 999px; + font-variant-numeric: tabular-nums; +} + +.settings-account-badge.is-active { + background: rgba(80, 180, 130, 0.14); + color: #6dc89a; +} + +.settings-account-badge.is-soon { + background: rgba(214, 170, 60, 0.16); + color: #e0bc5b; +} + +.settings-account-badge.is-expired { + background: rgba(224, 100, 75, 0.16); + color: var(--status-critical, #e0644b); +} + .settings-account-meta { font-size: 12px; color: var(--bg-ink); diff --git a/src/settings-store.ts b/src/settings-store.ts index 95826e3..11954fb 100644 --- a/src/settings-store.ts +++ b/src/settings-store.ts @@ -27,8 +27,10 @@ export interface ClaudeAccountSummary { export interface CodexAccountSummary { index: number; accountId: string | null; + email: string | null; planType: string | null; subscriptionUntil: string | null; + subscriptionLastChecked: string | null; exists: boolean; } @@ -105,8 +107,10 @@ function readCodexAccount(index: number): CodexAccountSummary | null { tokens?: { id_token?: string; access_token?: string }; }>(file); let accountId: string | null = null; + let email: string | null = null; let planType: string | null = null; let subscriptionUntil: string | null = null; + let subscriptionLastChecked: string | null = null; const idToken = data?.tokens?.id_token; if (idToken && typeof idToken === 'string') { const parts = idToken.split('.'); @@ -116,6 +120,7 @@ function readCodexAccount(index: number): CodexAccountSummary | null { Buffer.from(parts[1], 'base64').toString('utf-8'), ) as Record; if (typeof payload.sub === 'string') accountId = payload.sub; + if (typeof payload.email === 'string') email = payload.email; const auth = payload['https://api.openai.com/auth'] as | Record | undefined; @@ -126,6 +131,9 @@ function readCodexAccount(index: number): CodexAccountSummary | null { if (typeof auth.chatgpt_subscription_active_until === 'string') { subscriptionUntil = auth.chatgpt_subscription_active_until; } + if (typeof auth.chatgpt_subscription_last_checked === 'string') { + subscriptionLastChecked = auth.chatgpt_subscription_last_checked; + } } } catch { /* ignore parse errors */ @@ -135,8 +143,10 @@ function readCodexAccount(index: number): CodexAccountSummary | null { return { index, accountId, + email, planType, subscriptionUntil, + subscriptionLastChecked, exists: true, }; } @@ -149,6 +159,7 @@ export function listClaudeAccounts(): ClaudeAccountSummary[] { if (fs.existsSync(dir)) { const entries = fs.readdirSync(dir); const indices = entries + .filter((e) => /^\d+$/.test(e)) .map((e) => Number.parseInt(e, 10)) .filter((n) => Number.isFinite(n) && n >= 1) .sort((a, b) => a - b); @@ -168,6 +179,7 @@ export function listCodexAccounts(): CodexAccountSummary[] { if (fs.existsSync(dir)) { const entries = fs.readdirSync(dir); const indices = entries + .filter((e) => /^\d+$/.test(e)) .map((e) => Number.parseInt(e, 10)) .filter((n) => Number.isFinite(n) && n >= 1) .sort((a, b) => a - b);