@raingor/pi-web-switch 0.8.2 → 0.8.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.html CHANGED
@@ -9,8 +9,8 @@
9
9
  <link rel="manifest" href="./manifest.webmanifest" />
10
10
  <meta name="theme-color" content="#05090d" />
11
11
  <meta name="description" content="Web UI for pi coding agent — configuration management, session browser, and usage dashboard" />
12
- <script type="module" crossorigin src="./assets/main-C-vDjj0-.js"></script>
13
- <link rel="stylesheet" crossorigin href="./assets/main-Cqzld-td.css">
12
+ <script type="module" crossorigin src="./assets/main-4u-qsOhX.js"></script>
13
+ <link rel="stylesheet" crossorigin href="./assets/main-Cz1qHtDa.css">
14
14
  </head>
15
15
  <body>
16
16
  <div id="root"></div>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@raingor/pi-web-switch",
3
3
  "private": false,
4
- "version": "0.8.2",
4
+ "version": "0.8.3",
5
5
  "type": "module",
6
6
  "main": "dist/index.html",
7
7
  "description": "Web UI for pi coding agent — live configuration management, session browser, and memory viewer",
@@ -54,6 +54,87 @@ export function writeSettings(settings: any): boolean {
54
54
  }
55
55
  }
56
56
 
57
+ export interface CodexUsageWindow {
58
+ windowSeconds: number;
59
+ usedPercent: number;
60
+ remainingPercent: number;
61
+ resetAfterSeconds: number | null;
62
+ resetAt: number | null;
63
+ }
64
+
65
+ export interface CodexUsageStatus {
66
+ loggedIn: boolean;
67
+ provider: "openai-codex";
68
+ planType?: string;
69
+ primary?: CodexUsageWindow;
70
+ secondary?: CodexUsageWindow;
71
+ checkedAt: string;
72
+ error?: string;
73
+ }
74
+
75
+ let codexUsageStatusCache: { value: CodexUsageStatus; at: number } | null = null;
76
+ const CODEX_USAGE_STATUS_TTL_MS = 30_000;
77
+
78
+ function codexUsageWindow(value: any): CodexUsageWindow | undefined {
79
+ if (!value || typeof value !== "object" || typeof value.limit_window_seconds !== "number") return undefined;
80
+ const usedPercent = Math.min(100, Math.max(0, Number(value.used_percent) || 0));
81
+ return {
82
+ windowSeconds: value.limit_window_seconds,
83
+ usedPercent,
84
+ remainingPercent: Math.max(0, 100 - usedPercent),
85
+ resetAfterSeconds: typeof value.reset_after_seconds === "number" ? value.reset_after_seconds : null,
86
+ resetAt: typeof value.reset_at === "number" ? value.reset_at : null,
87
+ };
88
+ }
89
+
90
+ /**
91
+ * Read the locally saved openai-codex OAuth session and query OpenAI's own
92
+ * Codex usage endpoint. Only a sanitized quota summary leaves the server;
93
+ * OAuth access/refresh tokens are never returned or written anywhere.
94
+ */
95
+ export async function getCodexUsageStatus(force = false): Promise<CodexUsageStatus> {
96
+ if (!force && codexUsageStatusCache && Date.now() - codexUsageStatusCache.at < CODEX_USAGE_STATUS_TTL_MS) {
97
+ return codexUsageStatusCache.value;
98
+ }
99
+
100
+ const checkedAt = new Date().toISOString();
101
+ const codex = readAuth()?.["openai-codex"];
102
+ if (codex?.type !== "oauth" || typeof codex.access !== "string" || !codex.access || typeof codex.accountId !== "string" || !codex.accountId) {
103
+ return { loggedIn: false, provider: "openai-codex", checkedAt };
104
+ }
105
+
106
+ try {
107
+ const response = await fetch("https://chatgpt.com/backend-api/wham/usage", {
108
+ headers: {
109
+ Authorization: `Bearer ${codex.access}`,
110
+ "chatgpt-account-id": codex.accountId,
111
+ },
112
+ signal: AbortSignal.timeout(15_000),
113
+ });
114
+ if (!response.ok) {
115
+ const value: CodexUsageStatus = { loggedIn: true, provider: "openai-codex", checkedAt, error: `OpenAI returned ${response.status}` };
116
+ codexUsageStatusCache = { value, at: Date.now() };
117
+ return value;
118
+ }
119
+ const payload = await response.json() as any;
120
+ const rateLimit = payload?.rate_limit;
121
+ const value: CodexUsageStatus = {
122
+ loggedIn: true,
123
+ provider: "openai-codex",
124
+ planType: typeof payload?.plan_type === "string" ? payload.plan_type : undefined,
125
+ primary: codexUsageWindow(rateLimit?.primary_window),
126
+ secondary: codexUsageWindow(rateLimit?.secondary_window),
127
+ checkedAt,
128
+ };
129
+ codexUsageStatusCache = { value, at: Date.now() };
130
+ return value;
131
+ } catch {
132
+ const value: CodexUsageStatus = { loggedIn: true, provider: "openai-codex", checkedAt, error: "Unable to query OpenAI usage" };
133
+ codexUsageStatusCache = { value, at: Date.now() };
134
+ return value;
135
+ }
136
+ }
137
+
57
138
  // ─── Official Usage Query ──────────────────────────────
58
139
 
59
140
  export type OfficialUsageAuthMode = "auto" | "bearer" | "x-api-key" | "api-key";
@@ -14,6 +14,35 @@ import {
14
14
 
15
15
  // ─── Types ──────────────────────────────────────────────
16
16
 
17
+ interface CodexUsageStatus {
18
+ loggedIn: boolean;
19
+ provider: "openai-codex";
20
+ planType?: string;
21
+ primary?: { windowSeconds: number; usedPercent: number; remainingPercent: number; resetAfterSeconds: number | null; resetAt: number | null };
22
+ secondary?: { windowSeconds: number; usedPercent: number; remainingPercent: number; resetAfterSeconds: number | null; resetAt: number | null };
23
+ checkedAt: string;
24
+ error?: string;
25
+ }
26
+
27
+ function formatRemainingTime(seconds: number | null | undefined): string | null {
28
+ if (typeof seconds !== "number" || seconds < 0) return null;
29
+ const hours = Math.floor(seconds / 3600);
30
+ const minutes = Math.floor((seconds % 3600) / 60);
31
+ return hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`;
32
+ }
33
+
34
+ function formatResetAt(unixSeconds: number | null | undefined, lang: string): string | null {
35
+ if (typeof unixSeconds !== "number" || unixSeconds <= 0) return null;
36
+ return new Intl.DateTimeFormat(lang, {
37
+ year: "numeric",
38
+ month: "2-digit",
39
+ day: "2-digit",
40
+ hour: "2-digit",
41
+ minute: "2-digit",
42
+ hour12: false,
43
+ }).format(new Date(unixSeconds * 1000));
44
+ }
45
+
17
46
  interface UsageRangeData {
18
47
  totalTokens: number;
19
48
  totalInput: number;
@@ -364,6 +393,7 @@ export function DashboardPage() {
364
393
  const [providerSort, setProviderSort] = useState<{ key: string; dir: SortDir }>({ key: "totalCost", dir: "desc" });
365
394
  const [modelSort, setModelSort] = useState<{ key: string; dir: SortDir }>({ key: "totalCost", dir: "desc" });
366
395
  const [prevTotals, setPrevTotals] = useState<{ tokens: number; cost: number } | null>(null);
396
+ const [codexUsage, setCodexUsage] = useState<CodexUsageStatus | null>(null);
367
397
 
368
398
  const customInvalid = range === "custom" && !!customFrom && !!customTo && customFrom > customTo;
369
399
 
@@ -402,6 +432,22 @@ export function DashboardPage() {
402
432
 
403
433
  useEffect(() => { fetchData(); }, [fetchData]);
404
434
 
435
+ // Official Codex quotas use the locally logged-in OAuth session. The API
436
+ // returns only a sanitized summary; OAuth credentials never reach the UI.
437
+ useEffect(() => {
438
+ if (source !== "pi") return;
439
+ let cancelled = false;
440
+ const load = () => {
441
+ fetch("/api/pi/codex-usage-status")
442
+ .then((r) => r.json())
443
+ .then((status: CodexUsageStatus) => { if (!cancelled) setCodexUsage(status); })
444
+ .catch(() => { if (!cancelled) setCodexUsage(null); });
445
+ };
446
+ load();
447
+ const id = window.setInterval(load, 30_000);
448
+ return () => { cancelled = true; window.clearInterval(id); };
449
+ }, [source]);
450
+
405
451
  // Reset request-log pagination when the queried range or source changes
406
452
  useEffect(() => { setLogPage(1); }, [range, customFrom, customTo, source]);
407
453
 
@@ -659,6 +705,29 @@ export function DashboardPage() {
659
705
  ))}
660
706
  </div>
661
707
  {source === "chatgpt" && <span className="dashboard-source-note">{t("dashboard.source_chatgpt_note")}</span>}
708
+ {source === "pi" && codexUsage && (
709
+ <div className="ml-auto flex flex-wrap items-center gap-2 text-xs" style={{ color: "var(--muted-text)" }}>
710
+ <span className={cn("inline-flex items-center gap-1 font-medium", codexUsage.loggedIn ? "text-emerald-400" : "text-gray-500")}>
711
+ <span className="h-1.5 w-1.5 rounded-full bg-current" />
712
+ {codexUsage.loggedIn ? t("dashboard.codex_logged_in") : t("dashboard.codex_not_logged_in")}
713
+ </span>
714
+ {codexUsage.loggedIn && codexUsage.primary && codexUsage.secondary && (
715
+ <>
716
+ <span className="rounded border border-gray-700 px-2 py-1 font-mono">
717
+ {t("dashboard.codex_5h")}: {t("dashboard.codex_remaining", `${codexUsage.primary.remainingPercent}%`)}
718
+ {formatRemainingTime(codexUsage.primary.resetAfterSeconds) && ` · ${formatRemainingTime(codexUsage.primary.resetAfterSeconds)}`}
719
+ {formatResetAt(codexUsage.primary.resetAt, lang) && ` · ${t("dashboard.codex_resets", formatResetAt(codexUsage.primary.resetAt, lang)!)}`}
720
+ </span>
721
+ <span className="rounded border border-gray-700 px-2 py-1 font-mono">
722
+ {t("dashboard.codex_7d")}: {t("dashboard.codex_remaining", `${codexUsage.secondary.remainingPercent}%`)}
723
+ {formatRemainingTime(codexUsage.secondary.resetAfterSeconds) && ` · ${formatRemainingTime(codexUsage.secondary.resetAfterSeconds)}`}
724
+ {formatResetAt(codexUsage.secondary.resetAt, lang) && ` · ${t("dashboard.codex_resets", formatResetAt(codexUsage.secondary.resetAt, lang)!)}`}
725
+ </span>
726
+ </>
727
+ )}
728
+ {codexUsage.error && <span className="text-amber-400">{t("dashboard.codex_quota_unavailable")}</span>}
729
+ </div>
730
+ )}
662
731
  </div>
663
732
 
664
733
  {/* Custom Date Picker */}
@@ -9,6 +9,11 @@ export interface ChangelogEntry {
9
9
  }
10
10
 
11
11
  export const CHANGELOG: ChangelogEntry[] = [
12
+ {
13
+ version: "0.8.3",
14
+ date: "2026-08-31",
15
+ itemKeys: ["changelog.0_8_3_1", "changelog.0_8_3_2"],
16
+ },
12
17
  {
13
18
  version: "0.8.2",
14
19
  date: "2026-08-28",
@@ -139,6 +139,8 @@ const en: Record<string, string> = {
139
139
  "app.version": "pi-switch v0.7.0",
140
140
  "changelog.button": "Changelog",
141
141
  "changelog.title": "What's New",
142
+ "changelog.0_8_3_1": "Usage: shows the local openai-codex sign-in state under Pi.",
143
+ "changelog.0_8_3_2": "Shows official OpenAI 5-hour and 7-day quota remaining, countdowns, and exact reset timestamps.",
142
144
  "changelog.0_8_2_1": "Speed-test results now persist across page switches; the last selected provider is remembered.",
143
145
  "changelog.0_8_2_2": "Models that pass 100% can be added to their provider in one click (single or batch); already-configured ones are marked.",
144
146
  "changelog.0_8_2_3": "Newly added models default to disabled until manually enabled.",
@@ -205,6 +207,13 @@ const en: Record<string, string> = {
205
207
  // Dashboard
206
208
  "dashboard.title": "Usage Statistics",
207
209
  "dashboard.source_pi": "Pi",
210
+ "dashboard.codex_logged_in": "Signed in to openai-codex",
211
+ "dashboard.codex_not_logged_in": "Not signed in to openai-codex",
212
+ "dashboard.codex_5h": "5 hours",
213
+ "dashboard.codex_7d": "7 days",
214
+ "dashboard.codex_remaining": "{0} remaining",
215
+ "dashboard.codex_resets": "resets {0}",
216
+ "dashboard.codex_quota_unavailable": "Official quota unavailable",
208
217
  "dashboard.source_chatgpt": "ChatGPT",
209
218
  "dashboard.data_source": "Data Source",
210
219
  "dashboard.source_chatgpt_note": "Reads local calls from ~/.codex/sessions and archived_sessions.",
@@ -137,6 +137,8 @@ const ja: Record<string, string> = {
137
137
  "app.version": "pi-switch v0.7.0",
138
138
  "changelog.button": "変更履歴",
139
139
  "changelog.title": "更新情報",
140
+ "changelog.0_8_3_1": "使用状況:Pi の下にローカル openai-codex ログイン状態を表示。",
141
+ "changelog.0_8_3_2": "OpenAI公式の5時間・7日間クォータ残量、カウントダウン、正確なリセット日時を表示。",
140
142
  "changelog.0_8_2_1": "速度テスト結果をページ切替後も保持し、前回選択したプロバイダーも記憶。",
141
143
  "changelog.0_8_2_2": "100%合格モデルをワンクリックでプロバイダーに追加(単発/一括)、設定済みは自動マーク。",
142
144
  "changelog.0_8_2_3": "追加したモデルはデフォルト無効、手動で有効化。",
@@ -202,6 +204,13 @@ const ja: Record<string, string> = {
202
204
 
203
205
  "dashboard.title": "使用統計",
204
206
  "dashboard.source_pi": "Pi",
207
+ "dashboard.codex_logged_in": "openai-codex にログイン済み",
208
+ "dashboard.codex_not_logged_in": "openai-codex に未ログイン",
209
+ "dashboard.codex_5h": "5時間",
210
+ "dashboard.codex_7d": "7日間",
211
+ "dashboard.codex_remaining": "残り {0}",
212
+ "dashboard.codex_resets": "リセット {0}",
213
+ "dashboard.codex_quota_unavailable": "公式クォータは一時利用不可",
205
214
  "dashboard.source_chatgpt": "ChatGPT",
206
215
  "dashboard.data_source": "データソース",
207
216
  "dashboard.source_chatgpt_note": "~/.codex/sessions と archived_sessions のローカル呼び出し記録を読み取ります。",
@@ -137,6 +137,8 @@ const zhCN: Record<string, string> = {
137
137
  "app.version": "pi-switch v0.7.0",
138
138
  "changelog.button": "更新日志",
139
139
  "changelog.title": "版本更新说明",
140
+ "changelog.0_8_3_1": "使用统计:Pi 程序下显示 openai-codex 的本地登录状态。",
141
+ "changelog.0_8_3_2": "显示 OpenAI 官方 5小时与7天额度剩余百分比、倒计时及准确重置日期时间。",
140
142
  "changelog.0_8_2_1": "测速结果持久化:切换页面再回来结果不丢,并记住上次选中的供应商。",
141
143
  "changelog.0_8_2_2": "测速 100% 通过的模型支持一键加入对应供应商(单个/批量),已存在自动标记。",
142
144
  "changelog.0_8_2_3": "新添加的模型默认为禁用状态,需手动启用。",
@@ -202,6 +204,13 @@ const zhCN: Record<string, string> = {
202
204
 
203
205
  "dashboard.title": "使用统计",
204
206
  "dashboard.source_pi": "Pi 程序",
207
+ "dashboard.codex_logged_in": "已登录 openai-codex",
208
+ "dashboard.codex_not_logged_in": "未登录 openai-codex",
209
+ "dashboard.codex_5h": "5小时",
210
+ "dashboard.codex_7d": "7天",
211
+ "dashboard.codex_remaining": "剩余 {0}",
212
+ "dashboard.codex_resets": "重置于 {0}",
213
+ "dashboard.codex_quota_unavailable": "官方额度暂不可用",
205
214
  "dashboard.source_chatgpt": "ChatGPT",
206
215
  "dashboard.data_source": "数据来源",
207
216
  "dashboard.source_chatgpt_note": "读取 ~/.codex/sessions 与 archived_sessions 中的本地调用记录。",
@@ -136,6 +136,8 @@ const zhTW: Record<string, string> = {
136
136
  "app.version": "pi-switch v0.7.0",
137
137
  "changelog.button": "更新日誌",
138
138
  "changelog.title": "版本更新說明",
139
+ "changelog.0_8_3_1": "使用統計:Pi 程式下顯示 openai-codex 的本機登入狀態。",
140
+ "changelog.0_8_3_2": "顯示 OpenAI 官方 5小時與7天額度剩餘百分比、倒數及準確重置日期時間。",
139
141
  "changelog.0_8_2_1": "測速結果持久化:切換頁面再回來結果不丟,並記住上次選中的供應商。",
140
142
  "changelog.0_8_2_2": "測速 100% 通過的模型支援一鍵加入對應供應商(單個/批次),已存在自動標記。",
141
143
  "changelog.0_8_2_3": "新添加的模型預設為停用狀態,需手動啟用。",
@@ -201,6 +203,13 @@ const zhTW: Record<string, string> = {
201
203
 
202
204
  "dashboard.title": "使用統計",
203
205
  "dashboard.source_pi": "Pi 程式",
206
+ "dashboard.codex_logged_in": "已登入 openai-codex",
207
+ "dashboard.codex_not_logged_in": "未登入 openai-codex",
208
+ "dashboard.codex_5h": "5小時",
209
+ "dashboard.codex_7d": "7天",
210
+ "dashboard.codex_remaining": "剩餘 {0}",
211
+ "dashboard.codex_resets": "重置於 {0}",
212
+ "dashboard.codex_quota_unavailable": "官方額度暫時不可用",
204
213
  "dashboard.source_chatgpt": "ChatGPT",
205
214
  "dashboard.data_source": "資料來源",
206
215
  "dashboard.source_chatgpt_note": "讀取 ~/.codex/sessions 與 archived_sessions 中的本機呼叫記錄。",
package/vite.config.ts CHANGED
@@ -49,6 +49,13 @@ function piApiPlugin(): Plugin {
49
49
  res.setHeader("Content-Type", "application/json");
50
50
  res.end(JSON.stringify(data ?? {}));
51
51
  },
52
+ "GET /api/pi/codex-usage-status"(req, res) {
53
+ const force = new URL(req.url ?? "", "http://localhost").searchParams.get("refresh") === "1";
54
+ pi.getCodexUsageStatus(force).then((status) => {
55
+ res.setHeader("Content-Type", "application/json");
56
+ res.end(JSON.stringify(status));
57
+ });
58
+ },
52
59
  "GET /api/pi/official-usage-config"(_, res) {
53
60
  const config = pi.readOfficialUsageConfig();
54
61
  res.setHeader("Content-Type", "application/json");