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 }) {
계정 없음
) : (계정 없음
) : (
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