Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions gui/src/components/codex-account-pool-cards.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useT } from "../i18n/shared";
import { useT, useI18n } from "../i18n/shared";
import { IconAlert, IconPause, IconPlay, IconX } from "../icons";
import { displayAccountId } from "../lib/privacy";
import type { CodexAccountEntry } from "./codex-account-pool-types";
Expand Down Expand Up @@ -49,6 +49,7 @@ export function CodexAccountPoolCards({
doctorCopyOutcomeFor?: (accountId: string) => "copied" | "unavailable" | null;
}) {
const t = useT();
const { locale } = useI18n();
const isNext = (account: CodexAccountEntry) => !account.paused && activeId === account.id;

return (
Expand Down Expand Up @@ -125,7 +126,16 @@ export function CodexAccountPoolCards({
<IconX width={14} />
</button>
</div>
<div className="card-sub">{a.email}{a.plan ? ` · ${a.plan}` : ""} · {t("prov.accountId")}: {displayAccountId(a.id)}</div>
<div className="card-sub">{(() => {
const parts = [a.email, a.plan ? a.plan : "", `${t("prov.accountId")}: ${displayAccountId(a.id)}`].filter(Boolean);
if (typeof a.expiresAt === "number" && a.expiresAt > 0) {
const expDate = new Date(a.expiresAt);
if (!Number.isNaN(expDate.getTime())) {
parts.push(t("prov.expiresAt", { date: expDate.toLocaleDateString(locale, { month: "short", day: "numeric", year: "numeric" }) }));
}
}
return parts.join(" · ");
})()}</div>
{healthSummary && (
<div className="card-sub faint">{healthSummary}</div>
)}
Expand Down Expand Up @@ -156,6 +166,7 @@ export function CodexAccountPoolReauthBanner({
onReauth: () => void;
}) {
const t = useT();
const { locale } = useI18n();
return (
<div className="notice-warn" style={{ marginBottom: 12, display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12, flexWrap: "wrap" }}>
<span><IconAlert width={14} /> {t("codexAuth.tokenExpired")}</span>
Expand Down
11 changes: 10 additions & 1 deletion gui/src/components/provider-workspace/ProviderAuthPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,16 @@ export default function ProviderAuthPanel({
<span className={`pwi-auth-dot ${showReauth ? "pwi-auth-dot--warn" : account.active ? "pwi-auth-dot--ok" : "pwi-auth-dot--off"}`} aria-hidden="true" />
<span className="pwi-auth-row-copy">
<span className="pwi-auth-row-label">{label}</span>
<span className="pwi-auth-row-secondary">{[account.email, `${t("prov.accountId")}: ${maskedId}`].filter(Boolean).join(" · ")}</span>
<span className="pwi-auth-row-secondary">{(() => {
const parts = [account.email, `${t("prov.accountId")}: ${maskedId}`];
if (typeof account.expiresAt === "number" && account.expiresAt > 0) {
const expDate = new Date(account.expiresAt);
if (!Number.isNaN(expDate.getTime())) {
parts.push(t("prov.expiresAt", { date: expDate.toLocaleDateString(locale, { month: "short", day: "numeric", year: "numeric" }) }));
Comment on lines +222 to +225

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Refresh OAuth expiration dates after token rotation

When an OAuth credential is refreshed by ordinary proxy traffic or the optional token guardian while the Providers page remains mounted, this date stays at the old token's expiration. useProviderAccountPools fetches account sets only on initial provider-list discovery and explicit account actions (useProviderAccountPools.ts:244-256), unlike the Codex pool's 30-second polling, so a valid refreshed account can indefinitely display a past expiration date. Poll the account summaries or invalidate this state when credentials rotate.

Useful? React with 👍 / 👎.

}
}
return parts.filter(Boolean).join(" · ");
})()}</span>
{healthSummary && (
<span className="pwi-auth-row-secondary faint">{healthSummary}</span>
)}
Expand Down
1 change: 1 addition & 0 deletions gui/src/components/provider-workspace/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export type OAuthAccountRow = {
email?: string;
active: boolean;
needsReauth?: boolean;
expiresAt?: number;
health?: { status: OAuthAccountHealthStatus; reason?: string; until?: string };
healthLabel?: string;
healthSummary?: string;
Expand Down
3 changes: 2 additions & 1 deletion gui/src/i18n/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,8 @@ export const de: Record<TKey, string> = {
"prov.removeConfirm": "Anbieter \"{name}\" entfernen? Seine Modelle verschwinden aus Codex’ Auswahl.",
"prov.hasApiKey": "API-Schlüssel konfiguriert",
"prov.hasHeaders": "benutzerdefinierte Header konfiguriert",
"prov.accounts": "Konten ({n})",
"prov.accounts":
"prov.expiresAt": "Läuft ab: {date}", "Konten ({n})",
Comment on lines +331 to +332

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Fix broken prov.accounts object-literal syntax (same defect as en.ts).

Lines 331-332 have the same invalid object-literal shape as gui/src/i18n/en.ts:

"prov.accounts":
"prov.expiresAt": "Läuft ab: {date}", "Konten ({n})",

Biome confirms a parse error at line 332. Restore "prov.accounts": "Konten ({n})" as its own key and keep "prov.expiresAt": "Läuft ab: {date}" separate. See the consolidated comment for the shared root cause and per-file fixes.

🧰 Tools
🪛 Biome (2.5.6)

[error] 332-332: expected , but instead found :

(parse)


[error] 332-332: expected : but instead found ,

(parse)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@gui/src/i18n/de.ts` around lines 331 - 332, Fix the malformed translation
entries in the German locale by restoring "prov.accounts" as its own key with
the "Konten ({n})" value and keeping "prov.expiresAt" as a separate key with
"Läuft ab: {date}".

Source: Linters/SAST tools

"prov.accountsAria": "{name}-Konten umschalten",
"prov.accountActive": "Aktiv",
"prov.accountReauth": "Erneut anmelden",
Expand Down
3 changes: 2 additions & 1 deletion gui/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,8 @@ export const en = {
"prov.removeConfirm": "Remove provider \"{name}\"? Its models disappear from Codex's picker.",
"prov.hasApiKey": "api key configured",
"prov.hasHeaders": "custom headers configured",
"prov.accounts": "Accounts ({n})",
"prov.accounts":
"prov.expiresAt": "Expires: {date}", "Accounts ({n})",
Comment on lines +348 to +349

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Fix broken prov.accounts object-literal syntax — this file fails to compile.

Lines 348-349 read:

"prov.accounts":
"prov.expiresAt": "Expires: {date}", "Accounts ({n})",

This is invalid JavaScript/TypeScript object syntax. "prov.accounts": has no value assigned before the next key begins, and "Accounts ({n})" becomes an orphaned string literal with no key. Biome confirms this with a parse error at line 349 ("expected , but instead found :"). Because en.ts defines the base TKey type (export type TKey = keyof typeof en;), a parse failure here breaks not just this file but every locale file typed as Record<TKey, string> (de.ts, ja.ts, ko.ts, ru.ts, zh.ts), since they all import TKey from en.ts. This contradicts the PR's claim of "no TypeScript errors."

Restore the original "prov.accounts": "Accounts ({n})" entry and add "prov.expiresAt" as its own separate key.

🐛 Proposed fix
-  "prov.accounts":
-  "prov.expiresAt": "Expires: {date}", "Accounts ({n})",
+  "prov.accounts": "Accounts ({n})",
+  "prov.expiresAt": "Expires: {date}",
🧰 Tools
🪛 Biome (2.5.6)

[error] 349-349: expected , but instead found :

(parse)


[error] 349-349: expected : but instead found ,

(parse)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@gui/src/i18n/en.ts` around lines 348 - 349, Fix the malformed entries in the
en locale object by restoring "prov.accounts" with the value "Accounts ({n})"
and defining "prov.expiresAt" as a separate key with its existing expiration
text. Preserve valid object-literal syntax so the exported TKey type and
dependent locale files compile.

Source: Linters/SAST tools

"prov.accountsAria": "Toggle {name} accounts",
"prov.accountActive": "Active",
"prov.accountReauth": "Re-login",
Expand Down
3 changes: 2 additions & 1 deletion gui/src/i18n/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,8 @@ export const ja: Record<TKey, string> = {
"prov.removeConfirm": "プロバイダー \"{name}\" を削除しますか? そのモデルは Codex のピッカーから消えます。",
"prov.hasApiKey": "API キー設定済み",
"prov.hasHeaders": "カスタムヘッダー設定済み",
"prov.accounts": "アカウント ({n})",
"prov.accounts":
"prov.expiresAt": "有効期限: {date}", "アカウント ({n})",
Comment on lines +337 to +338

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Fix broken prov.accounts object-literal syntax (same defect as en.ts).

Lines 337-338 have the same invalid object-literal shape as gui/src/i18n/en.ts:

"prov.accounts":
"prov.expiresAt": "有効期限: {date}", "アカウント ({n})",

Biome confirms a parse error at line 338. Restore "prov.accounts": "アカウント ({n})" as its own key and keep "prov.expiresAt" separate. See the consolidated comment for the shared root cause and per-file fixes.

🧰 Tools
🪛 Biome (2.5.6)

[error] 338-338: expected , but instead found :

(parse)


[error] 338-338: expected : but instead found ,

(parse)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@gui/src/i18n/ja.ts` around lines 337 - 338, Fix the malformed translation
entries in the Japanese locale by making "prov.accounts" its own key with the
value "アカウント ({n})", while keeping "prov.expiresAt" as a separate key with its
existing value.

Source: Linters/SAST tools

"prov.accountsAria": "{name} のアカウントを切り替え",
"prov.accountActive": "アクティブ",
"prov.accountReauth": "再ログイン",
Expand Down
3 changes: 2 additions & 1 deletion gui/src/i18n/ko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,8 @@ export const ko: Record<TKey, string> = {
"prov.removeConfirm": "프로바이더 \"{name}\" 을(를) 삭제할까요? 해당 모델이 Codex 선택기에서 사라집니다.",
"prov.hasApiKey": "API 키 설정됨",
"prov.hasHeaders": "커스텀 헤더 설정됨",
"prov.accounts": "계정 ({n})",
"prov.accounts":
"prov.expiresAt": "만료일: {date}", "계정 ({n})",
Comment on lines +340 to +341

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Fix broken prov.accounts object-literal syntax (same defect as en.ts).

Lines 340-341 have the same invalid object-literal shape as gui/src/i18n/en.ts:

"prov.accounts":
"prov.expiresAt": "만료일: {date}", "계정 ({n})",

Biome confirms a parse error at line 341. Restore "prov.accounts": "계정 ({n})" as its own key and keep "prov.expiresAt" separate. See the consolidated comment for the shared root cause and per-file fixes.

🧰 Tools
🪛 Biome (2.5.6)

[error] 341-341: expected , but instead found :

(parse)


[error] 341-341: expected : but instead found ,

(parse)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@gui/src/i18n/ko.ts` around lines 340 - 341, Fix the object-literal entries in
the Korean translation object by restoring "prov.accounts" with the value "계정
({n})" and keeping "prov.expiresAt" with its existing value as a separate
key-value entry. Ensure the surrounding syntax parses correctly.

Source: Linters/SAST tools

"prov.accountsAria": "{name} 계정 목록 열기/닫기",
"prov.accountActive": "활성",
"prov.accountReauth": "재로그인",
Expand Down
3 changes: 2 additions & 1 deletion gui/src/i18n/ru.ts
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,8 @@ export const ru: Record<TKey, string> = {
"prov.removeConfirm": "Удалить провайдера \"{name}\"? Его модели исчезнут из селектора моделей Codex.",
"prov.hasApiKey": "API-ключ настроен",
"prov.hasHeaders": "настроены пользовательские заголовки",
"prov.accounts": "Аккаунты ({n})",
"prov.accounts":
"prov.expiresAt": "Истекает: {date}", "Аккаунты ({n})",
Comment on lines +342 to +343

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Fix broken prov.accounts object-literal syntax (same defect as en.ts).

Lines 342-343 have the same invalid object-literal shape as gui/src/i18n/en.ts:

"prov.accounts":
"prov.expiresAt": "Истекает: {date}", "Аккаунты ({n})",

Biome confirms a parse error at line 343. Restore "prov.accounts": "Аккаунты ({n})" as its own key and keep "prov.expiresAt" separate. See the consolidated comment for the shared root cause and per-file fixes.

🧰 Tools
🪛 Biome (2.5.6)

[error] 343-343: expected , but instead found :

(parse)


[error] 343-343: expected : but instead found ,

(parse)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@gui/src/i18n/ru.ts` around lines 342 - 343, Fix the object-literal entries in
the Russian translations by restoring "prov.accounts" with the value "Аккаунты
({n})" as its own key, and keep "prov.expiresAt" mapped separately to "Истекает:
{date}" within the i18n object.

Source: Linters/SAST tools

"prov.accountsAria": "Показать или скрыть аккаунты {name}",
"prov.accountActive": "Активен",
"prov.accountReauth": "Повторный вход",
Expand Down
3 changes: 2 additions & 1 deletion gui/src/i18n/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,8 @@ export const zh: Record<TKey, string> = {
"prov.removeConfirm": "移除提供方 \"{name}\"?其模型将从 Codex 选择器中消失。",
"prov.hasApiKey": "已配置 API 密钥",
"prov.hasHeaders": "已配置自定义请求头",
"prov.accounts": "账户({n})",
"prov.accounts":
"prov.expiresAt": "到期时间: {date}", "账户({n})",
Comment on lines +337 to +338

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Fix broken prov.accounts object-literal syntax (same defect as en.ts).

Lines 337-338 have the same invalid object-literal shape as gui/src/i18n/en.ts:

"prov.accounts":
"prov.expiresAt": "到期时间: {date}", "账户({n})",

Biome confirms a parse error at line 338. Restore "prov.accounts": "账户({n})" as its own key and keep "prov.expiresAt" separate. See the consolidated comment for the shared root cause and per-file fixes.

🧰 Tools
🪛 Biome (2.5.6)

[error] 338-338: expected , but instead found :

(parse)


[error] 338-338: expected : but instead found ,

(parse)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@gui/src/i18n/zh.ts` around lines 337 - 338, Fix the malformed translation
entries in the zh.ts locale object: make "prov.accounts" a standalone key with
value "账户({n})", and keep "prov.expiresAt" as a separate key with its existing
value "到期时间: {date}".

Source: Linters/SAST tools

"prov.accountsAria": "展开/收起 {name} 账户",
"prov.accountActive": "使用中",
"prov.accountReauth": "需重新登录",
Expand Down
3 changes: 3 additions & 0 deletions src/codex/auth-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ function poolAccountDto(
const quota = quotaForPlan(quotaResult.quota, account.plan);
const needsReauth = !hasCredential || quotaResult.needsReauth || isAccountNeedsReauth(account.id);
const health = projectCodexAccountHealth({ accountId: account.id, needsReauth });
const cred = getCodexAccountCredential(account.id);
return {
id: account.id,
email: maskEmail(account.email) ?? account.email,
Expand All @@ -206,6 +207,7 @@ function poolAccountDto(
quota: quota ? { ...quota } : null,
needsReauth,
hasCredential,
...(cred?.expiresAt ? { expiresAt: cred.expiresAt } : {}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Populate expiration for the main Codex account

When the dashboard uses the default Codex App login, this field is never returned: poolAccountDto only handles configured pool accounts, while the separately constructed main-account DTO at auth-api.ts:1055-1069 omits expiresAt. The main access token's JWT expiration is available and is already decoded in main-account.ts:34-39, so derive and include it in the main DTO as well; otherwise the new expiration display does nothing for the primary account row.

Useful? React with 👍 / 👎.

...(quotaResult.quotaProbeSkipped ? { quotaProbeSkipped: true as const } : {}),
...oauthAccountHealthFields("codex", account.id, health),
};
Expand Down Expand Up @@ -647,6 +649,7 @@ export interface CodexAuthAccountDto {
quota: (StoredAccountQuota | (Omit<StoredAccountQuota, "updatedAt"> & { updatedAt: number })) | null;
needsReauth?: boolean;
hasCredential: boolean;
expiresAt?: number;
health: OAuthAccountHealth;
healthLabel: OAuthHealthLabel;
healthSummary: string;
Expand Down
1 change: 1 addition & 0 deletions tests/oauth-accounts-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ describe("multiauth accounts API", () => {
const emails = body.accounts.map(a => a.email ?? "");
expect(emails.some(e => e.includes("first@example.com"))).toBe(false); // masked
expect(body.accounts.find(a => a.id === "aaaa1111")?.active).toBe(true);
expect(body.accounts.find(a => a.id === "aaaa1111")?.expiresAt).toBe(9999999999999);
const raw = JSON.stringify(body);
expect(raw.includes("t1")).toBe(false); // no tokens
} finally {
Expand Down
Loading