icebear0828 Claude Opus 4.6 commited on
Commit
06a5304
·
1 Parent(s): c52388f

feat: GitHub Star badge + check-for-updates in dashboard header

Browse files

- Header: ⭐ Star badge linking to GitHub repo (amber pill style)
- Header: 🔄 Check for Updates button with status display
- Checks both proxy self-update (git fetch/pull/build) and Codex fingerprint (appcast)
- CLI: auto git pull + npm install + build, prompt restart
- Docker: fingerprint auto-update, proxy shows manual rebuild hint
- Electron: version display only, managed by desktop app
- New endpoints: GET /admin/update-status, POST /admin/check-update
- Footer: version info (proxy commit + Codex Desktop version)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

CHANGELOG.md CHANGED
@@ -8,6 +8,15 @@
8
 
9
  ### Added
10
 
 
 
 
 
 
 
 
 
 
11
  - GPT-5.4 + Codex Spark 模型支持:新增 `gpt-5.4`(4 种 effort: minimal/low/medium/high)和 `gpt-5.3-codex-spark`(minimal/low),`codex` 别名更新为 `gpt-5.4`
12
  - 扩展推理等级:支持 `minimal`、`xhigh` 等新 effort 值,客户端发送的任意 `reasoning_effort` 均透传到后端
13
  - 模型家族矩阵选择器:Dashboard 模型选择从平面下拉改为家族列表 + 推理等级按钮组,通过 `/v1/models/catalog` 端点获取完整目录
 
8
 
9
  ### Added
10
 
11
+ - Dashboard GitHub Star 徽章:Header 新增醒目的 ⭐ Star 按钮(amber 药丸样式),点击跳转 GitHub 仓库页面,方便用户收藏和获取更新
12
+ - Dashboard 检查更新功能:Footer 显示 Proxy 版本+commit 和 Codex Desktop 指纹版本,提供"检查更新"按钮同时检查两种更新
13
+ - Proxy 自更新(CLI 模式):通过 `git fetch` 检查新提交,自动执行 `git pull + npm install + npm run build`,完成后提示重启
14
+ - Codex 指纹更新:手动触发现有 appcast 检查,自动应用指纹/模型配置变更
15
+ - Docker 兼容:指纹可自动更新,代理代码提示手动 `docker compose up -d --build`
16
+ - Electron 兼容:显示版本信息,更新由桌面应用管理
17
+ - `GET /admin/update-status` 端点:返回 proxy 和 codex 两种更新的当前状态
18
+ - `POST /admin/check-update` 端点:同时触发 proxy 自检 + codex 指纹检查,自动应用可用更新
19
+ - `src/self-update.ts`:Proxy 自更新模块(git 子进程实现,支持检查/拉取/构建)
20
  - GPT-5.4 + Codex Spark 模型支持:新增 `gpt-5.4`(4 种 effort: minimal/low/medium/high)和 `gpt-5.3-codex-spark`(minimal/low),`codex` 别名更新为 `gpt-5.4`
21
  - 扩展推理等级:支持 `minimal`、`xhigh` 等新 effort 值,客户端发送的任意 `reasoning_effort` 均透传到后端
22
  - 模型家族矩阵选择器:Dashboard 模型选择从平面下拉改为家族列表 + 推理等级按钮组,通过 `/v1/models/catalog` 端点获取完整目录
shared/hooks/use-update-status.ts ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect, useCallback } from "preact/hooks";
2
+
3
+ export interface UpdateStatus {
4
+ proxy: {
5
+ version: string;
6
+ commit: string | null;
7
+ can_self_update: boolean;
8
+ commits_behind: number | null;
9
+ update_in_progress: boolean;
10
+ };
11
+ codex: {
12
+ current_version: string | null;
13
+ current_build: string | null;
14
+ latest_version: string | null;
15
+ latest_build: string | null;
16
+ update_available: boolean;
17
+ update_in_progress: boolean;
18
+ last_check: string | null;
19
+ };
20
+ }
21
+
22
+ export interface CheckResult {
23
+ proxy?: {
24
+ commits_behind: number;
25
+ current_commit: string | null;
26
+ latest_commit: string | null;
27
+ update_applied?: boolean;
28
+ error?: string;
29
+ };
30
+ codex?: {
31
+ update_available: boolean;
32
+ current_version: string;
33
+ latest_version: string | null;
34
+ error?: string;
35
+ };
36
+ proxy_update_in_progress: boolean;
37
+ codex_update_in_progress: boolean;
38
+ }
39
+
40
+ export function useUpdateStatus() {
41
+ const [status, setStatus] = useState<UpdateStatus | null>(null);
42
+ const [checking, setChecking] = useState(false);
43
+ const [result, setResult] = useState<CheckResult | null>(null);
44
+ const [error, setError] = useState<string | null>(null);
45
+
46
+ const load = useCallback(async () => {
47
+ try {
48
+ const resp = await fetch("/admin/update-status");
49
+ if (resp.ok) {
50
+ setStatus(await resp.json());
51
+ }
52
+ } catch { /* ignore */ }
53
+ }, []);
54
+
55
+ useEffect(() => { load(); }, [load]);
56
+
57
+ const checkForUpdate = useCallback(async () => {
58
+ setChecking(true);
59
+ setError(null);
60
+ setResult(null);
61
+ try {
62
+ const resp = await fetch("/admin/check-update", { method: "POST" });
63
+ const data: CheckResult = await resp.json();
64
+ if (!resp.ok) {
65
+ setError("Check failed");
66
+ } else {
67
+ setResult(data);
68
+ // Refresh status after check
69
+ await load();
70
+ }
71
+ } catch (err) {
72
+ setError(err instanceof Error ? err.message : "Network error");
73
+ } finally {
74
+ setChecking(false);
75
+ }
76
+ }, [load]);
77
+
78
+ return { status, checking, result, error, checkForUpdate };
79
+ }
shared/i18n/translations.ts CHANGED
@@ -59,6 +59,19 @@ export const translations = {
59
  refresh: "Refresh",
60
  updatedAt: "Updated at",
61
  footer: "\u00a9 2025 Codex Proxy. All rights reserved.",
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  },
63
  zh: {
64
  serverOnline: "\u670d\u52a1\u8fd0\u884c\u4e2d",
@@ -123,6 +136,19 @@ export const translations = {
123
  updatedAt: "\u66f4\u65b0\u4e8e",
124
  footer:
125
  "\u00a9 2025 Codex Proxy\u3002\u4fdd\u7559\u6240\u6709\u6743\u5229\u3002",
 
 
 
 
 
 
 
 
 
 
 
 
 
126
  },
127
  } as const;
128
 
 
59
  refresh: "Refresh",
60
  updatedAt: "Updated at",
61
  footer: "\u00a9 2025 Codex Proxy. All rights reserved.",
62
+ starOnGithub: "Star",
63
+ checkForUpdates: "Check for Updates",
64
+ checkingUpdates: "Checking...",
65
+ upToDate: "Up to date",
66
+ updateApplied: "Update complete, please restart server",
67
+ fingerprintUpdated: "Fingerprint updated",
68
+ proxyUpdating: "Updating proxy...",
69
+ proxyBehind: "behind",
70
+ dockerUpdateHint: "Run: docker compose up -d --build",
71
+ managedByDesktop: "Managed by desktop app",
72
+ lastChecked: "Last checked",
73
+ never: "never",
74
+ commits: "commits",
75
  },
76
  zh: {
77
  serverOnline: "\u670d\u52a1\u8fd0\u884c\u4e2d",
 
136
  updatedAt: "\u66f4\u65b0\u4e8e",
137
  footer:
138
  "\u00a9 2025 Codex Proxy\u3002\u4fdd\u7559\u6240\u6709\u6743\u5229\u3002",
139
+ starOnGithub: "\u6536\u85cf",
140
+ checkForUpdates: "\u68c0\u67e5\u66f4\u65b0",
141
+ checkingUpdates: "\u68c0\u67e5\u4e2d...",
142
+ upToDate: "\u5df2\u662f\u6700\u65b0",
143
+ updateApplied: "\u66f4\u65b0\u5b8c\u6210\uff0c\u8bf7\u91cd\u542f\u670d\u52a1",
144
+ fingerprintUpdated: "\u6307\u7eb9\u5df2\u66f4\u65b0",
145
+ proxyUpdating: "\u6b63\u5728\u66f4\u65b0\u4ee3\u7406...",
146
+ proxyBehind: "\u843d\u540e",
147
+ dockerUpdateHint: "\u8bf7\u6267\u884c: docker compose up -d --build",
148
+ managedByDesktop: "\u7531\u684c\u9762\u5e94\u7528\u7ba1\u7406",
149
+ lastChecked: "\u4e0a\u6b21\u68c0\u67e5",
150
+ never: "\u4ece\u672a",
151
+ commits: "\u4e2a\u63d0\u4ea4",
152
  },
153
  } as const;
154
 
src/routes/web.ts CHANGED
@@ -4,7 +4,9 @@ import { readFileSync, existsSync } from "fs";
4
  import { resolve } from "path";
5
  import type { AccountPool } from "../auth/account-pool.js";
6
  import { getConfig, getFingerprint } from "../config.js";
7
- import { getPublicDir, getDesktopPublicDir, getConfigDir, getDataDir } from "../paths.js";
 
 
8
 
9
  export function createWebRoutes(accountPool: AccountPool): Hono {
10
  const app = new Hono();
@@ -120,5 +122,89 @@ export function createWebRoutes(accountPool: AccountPool): Hono {
120
  });
121
  });
122
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
  return app;
124
  }
 
4
  import { resolve } from "path";
5
  import type { AccountPool } from "../auth/account-pool.js";
6
  import { getConfig, getFingerprint } from "../config.js";
7
+ import { getPublicDir, getDesktopPublicDir, getConfigDir, getDataDir, isEmbedded } from "../paths.js";
8
+ import { getUpdateState, checkForUpdate, isUpdateInProgress } from "../update-checker.js";
9
+ import { getProxyInfo, canSelfUpdate, checkProxySelfUpdate, applyProxySelfUpdate, isProxyUpdateInProgress } from "../self-update.js";
10
 
11
  export function createWebRoutes(accountPool: AccountPool): Hono {
12
  const app = new Hono();
 
122
  });
123
  });
124
 
125
+ // --- Update management endpoints ---
126
+
127
+ app.get("/admin/update-status", (c) => {
128
+ const proxyInfo = getProxyInfo();
129
+ const codexState = getUpdateState();
130
+
131
+ return c.json({
132
+ proxy: {
133
+ version: proxyInfo.version,
134
+ commit: proxyInfo.commit,
135
+ can_self_update: canSelfUpdate(),
136
+ commits_behind: null as number | null,
137
+ update_in_progress: isProxyUpdateInProgress(),
138
+ },
139
+ codex: {
140
+ current_version: codexState?.current_version ?? null,
141
+ current_build: codexState?.current_build ?? null,
142
+ latest_version: codexState?.latest_version ?? null,
143
+ latest_build: codexState?.latest_build ?? null,
144
+ update_available: codexState?.update_available ?? false,
145
+ update_in_progress: isUpdateInProgress(),
146
+ last_check: codexState?.last_check ?? null,
147
+ },
148
+ });
149
+ });
150
+
151
+ app.post("/admin/check-update", async (c) => {
152
+ const results: {
153
+ proxy?: { commits_behind: number; current_commit: string | null; latest_commit: string | null; update_applied?: boolean; error?: string };
154
+ codex?: { update_available: boolean; current_version: string; latest_version: string | null; error?: string };
155
+ } = {};
156
+
157
+ // 1. Proxy self-update check
158
+ if (canSelfUpdate()) {
159
+ try {
160
+ const proxyResult = await checkProxySelfUpdate();
161
+ results.proxy = {
162
+ commits_behind: proxyResult.commitsBehind,
163
+ current_commit: proxyResult.currentCommit,
164
+ latest_commit: proxyResult.latestCommit,
165
+ };
166
+
167
+ // Auto-apply if behind
168
+ if (proxyResult.commitsBehind > 0 && !isProxyUpdateInProgress()) {
169
+ const applyResult = await applyProxySelfUpdate();
170
+ results.proxy.update_applied = applyResult.started;
171
+ if (applyResult.error) results.proxy.error = applyResult.error;
172
+ }
173
+ } catch (err) {
174
+ results.proxy = {
175
+ commits_behind: 0,
176
+ current_commit: null,
177
+ latest_commit: null,
178
+ error: err instanceof Error ? err.message : String(err),
179
+ };
180
+ }
181
+ }
182
+
183
+ // 2. Codex fingerprint check
184
+ if (!isEmbedded()) {
185
+ try {
186
+ const codexState = await checkForUpdate();
187
+ results.codex = {
188
+ update_available: codexState.update_available,
189
+ current_version: codexState.current_version,
190
+ latest_version: codexState.latest_version,
191
+ };
192
+ } catch (err) {
193
+ results.codex = {
194
+ update_available: false,
195
+ current_version: "unknown",
196
+ latest_version: null,
197
+ error: err instanceof Error ? err.message : String(err),
198
+ };
199
+ }
200
+ }
201
+
202
+ return c.json({
203
+ ...results,
204
+ proxy_update_in_progress: isProxyUpdateInProgress(),
205
+ codex_update_in_progress: isUpdateInProgress(),
206
+ });
207
+ });
208
+
209
  return app;
210
  }
src/self-update.ts ADDED
@@ -0,0 +1,150 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Proxy self-update — checks for new commits on GitHub and applies via git pull.
3
+ * Only works in CLI mode (where .git exists). Docker/Electron show manual instructions.
4
+ */
5
+
6
+ import { execFile, execFileSync } from "child_process";
7
+ import { existsSync, readFileSync } from "fs";
8
+ import { resolve } from "path";
9
+ import { promisify } from "util";
10
+ import { isEmbedded } from "./paths.js";
11
+
12
+ const execFileAsync = promisify(execFile);
13
+
14
+ export interface ProxyInfo {
15
+ version: string;
16
+ commit: string | null;
17
+ }
18
+
19
+ export interface ProxySelfUpdateResult {
20
+ commitsBehind: number;
21
+ currentCommit: string | null;
22
+ latestCommit: string | null;
23
+ }
24
+
25
+ let _proxyUpdateInProgress = false;
26
+ let _gitAvailable: boolean | null = null;
27
+
28
+ /** Read proxy version from package.json + current git commit hash. */
29
+ export function getProxyInfo(): ProxyInfo {
30
+ let version = "unknown";
31
+ try {
32
+ const pkg = JSON.parse(readFileSync(resolve(process.cwd(), "package.json"), "utf-8"));
33
+ version = pkg.version ?? "unknown";
34
+ } catch { /* ignore */ }
35
+
36
+ let commit: string | null = null;
37
+ if (canSelfUpdate()) {
38
+ try {
39
+ const out = execFileSync("git", ["rev-parse", "--short", "HEAD"], {
40
+ cwd: process.cwd(),
41
+ encoding: "utf-8",
42
+ timeout: 5000,
43
+ });
44
+ commit = out.trim() || null;
45
+ } catch { /* ignore */ }
46
+ }
47
+
48
+ return { version, commit };
49
+ }
50
+
51
+ /** Whether this environment supports git-based self-update. */
52
+ export function canSelfUpdate(): boolean {
53
+ if (isEmbedded()) return false;
54
+ if (_gitAvailable !== null) return _gitAvailable;
55
+
56
+ // Check .git directory exists
57
+ if (!existsSync(resolve(process.cwd(), ".git"))) {
58
+ _gitAvailable = false;
59
+ return false;
60
+ }
61
+
62
+ // Check git command is available
63
+ try {
64
+ execFileSync("git", ["--version"], {
65
+ cwd: process.cwd(),
66
+ timeout: 5000,
67
+ stdio: "ignore",
68
+ });
69
+ _gitAvailable = true;
70
+ } catch {
71
+ _gitAvailable = false;
72
+ }
73
+
74
+ return _gitAvailable;
75
+ }
76
+
77
+ /** Whether a proxy self-update is currently in progress. */
78
+ export function isProxyUpdateInProgress(): boolean {
79
+ return _proxyUpdateInProgress;
80
+ }
81
+
82
+ /** Fetch latest from origin and check how many commits behind. */
83
+ export async function checkProxySelfUpdate(): Promise<ProxySelfUpdateResult> {
84
+ const cwd = process.cwd();
85
+
86
+ // Get current commit
87
+ let currentCommit: string | null = null;
88
+ try {
89
+ const { stdout } = await execFileAsync("git", ["rev-parse", "--short", "HEAD"], { cwd, timeout: 5000 });
90
+ currentCommit = stdout.trim() || null;
91
+ } catch { /* ignore */ }
92
+
93
+ // Fetch latest
94
+ try {
95
+ await execFileAsync("git", ["fetch", "origin", "master", "--quiet"], { cwd, timeout: 30000 });
96
+ } catch (err) {
97
+ console.warn("[SelfUpdate] git fetch failed:", err instanceof Error ? err.message : err);
98
+ return { commitsBehind: 0, currentCommit, latestCommit: currentCommit };
99
+ }
100
+
101
+ // Count commits behind
102
+ let commitsBehind = 0;
103
+ let latestCommit: string | null = null;
104
+ try {
105
+ const { stdout: countOut } = await execFileAsync(
106
+ "git", ["rev-list", "HEAD..origin/master", "--count"], { cwd, timeout: 5000 },
107
+ );
108
+ commitsBehind = parseInt(countOut.trim(), 10) || 0;
109
+
110
+ const { stdout: latestOut } = await execFileAsync(
111
+ "git", ["rev-parse", "--short", "origin/master"], { cwd, timeout: 5000 },
112
+ );
113
+ latestCommit = latestOut.trim() || null;
114
+ } catch { /* ignore */ }
115
+
116
+ return { commitsBehind, currentCommit, latestCommit };
117
+ }
118
+
119
+ /**
120
+ * Apply proxy self-update: git pull + npm install + npm run build.
121
+ * Runs in background. Returns immediately; check isProxyUpdateInProgress() for status.
122
+ */
123
+ export async function applyProxySelfUpdate(): Promise<{ started: boolean; error?: string }> {
124
+ if (_proxyUpdateInProgress) {
125
+ return { started: false, error: "Update already in progress" };
126
+ }
127
+
128
+ _proxyUpdateInProgress = true;
129
+ const cwd = process.cwd();
130
+
131
+ try {
132
+ console.log("[SelfUpdate] Pulling latest code...");
133
+ await execFileAsync("git", ["pull", "origin", "master"], { cwd, timeout: 60000 });
134
+
135
+ console.log("[SelfUpdate] Installing dependencies...");
136
+ await execFileAsync("npm", ["install"], { cwd, timeout: 120000, shell: true });
137
+
138
+ console.log("[SelfUpdate] Building...");
139
+ await execFileAsync("npm", ["run", "build"], { cwd, timeout: 120000, shell: true });
140
+
141
+ console.log("[SelfUpdate] Update complete. Server restart required.");
142
+ _proxyUpdateInProgress = false;
143
+ return { started: true };
144
+ } catch (err) {
145
+ _proxyUpdateInProgress = false;
146
+ const msg = err instanceof Error ? err.message : String(err);
147
+ console.error("[SelfUpdate] Update failed:", msg);
148
+ return { started: false, error: msg };
149
+ }
150
+ }
src/update-checker.ts CHANGED
@@ -206,6 +206,11 @@ export function getUpdateState(): UpdateState | null {
206
  return _currentState;
207
  }
208
 
 
 
 
 
 
209
  function scheduleNextPoll(): void {
210
  _pollTimer = setTimeout(() => {
211
  checkForUpdate().catch((err) => {
 
206
  return _currentState;
207
  }
208
 
209
+ /** Whether a full-update pipeline is currently running. */
210
+ export function isUpdateInProgress(): boolean {
211
+ return _updateInProgress;
212
+ }
213
+
214
  function scheduleNextPoll(): void {
215
  _pollTimer = setTimeout(() => {
216
  checkForUpdate().catch((err) => {
web/src/App.tsx CHANGED
@@ -9,14 +9,61 @@ import { CodeExamples } from "./components/CodeExamples";
9
  import { Footer } from "./components/Footer";
10
  import { useAccounts } from "../../shared/hooks/use-accounts";
11
  import { useStatus } from "../../shared/hooks/use-status";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
 
13
  function Dashboard() {
14
  const accounts = useAccounts();
15
  const status = useStatus(accounts.list.length);
 
16
 
17
  return (
18
  <>
19
- <Header onAddAccount={accounts.startAdd} />
 
 
 
 
 
 
20
  <main class="flex-grow px-4 md:px-8 lg:px-40 py-8 flex justify-center">
21
  <div class="flex flex-col w-full max-w-[960px] gap-6">
22
  <AddAccount
@@ -55,7 +102,7 @@ function Dashboard() {
55
  />
56
  </div>
57
  </main>
58
- <Footer />
59
  </>
60
  );
61
  }
 
9
  import { Footer } from "./components/Footer";
10
  import { useAccounts } from "../../shared/hooks/use-accounts";
11
  import { useStatus } from "../../shared/hooks/use-status";
12
+ import { useUpdateStatus } from "../../shared/hooks/use-update-status";
13
+ import { useI18n } from "../../shared/i18n/context";
14
+
15
+ function useUpdateMessage() {
16
+ const { t } = useI18n();
17
+ const update = useUpdateStatus();
18
+
19
+ let msg: string | null = null;
20
+ let color = "text-primary";
21
+
22
+ if (!update.checking && update.result) {
23
+ const parts: string[] = [];
24
+ const r = update.result;
25
+
26
+ if (r.proxy?.error) {
27
+ parts.push(`Proxy: ${r.proxy.error}`);
28
+ color = "text-red-500";
29
+ } else if (r.proxy?.update_applied) {
30
+ parts.push(t("updateApplied"));
31
+ color = "text-blue-500";
32
+ } else if (r.proxy && r.proxy.commits_behind > 0) {
33
+ parts.push(`Proxy: ${r.proxy.commits_behind} ${t("commits")} ${t("proxyBehind")}`);
34
+ color = "text-amber-500";
35
+ }
36
+
37
+ if (r.codex?.error) {
38
+ parts.push(`Codex: ${r.codex.error}`);
39
+ color = "text-red-500";
40
+ } else if (r.codex_update_in_progress) {
41
+ parts.push(t("fingerprintUpdated"));
42
+ }
43
+
44
+ msg = parts.length > 0 ? parts.join(" · ") : t("upToDate");
45
+ } else if (!update.checking && update.error) {
46
+ msg = update.error;
47
+ color = "text-red-500";
48
+ }
49
+
50
+ return { ...update, msg, color };
51
+ }
52
 
53
  function Dashboard() {
54
  const accounts = useAccounts();
55
  const status = useStatus(accounts.list.length);
56
+ const update = useUpdateMessage();
57
 
58
  return (
59
  <>
60
+ <Header
61
+ onAddAccount={accounts.startAdd}
62
+ onCheckUpdate={update.checkForUpdate}
63
+ checking={update.checking}
64
+ updateStatusMsg={update.msg}
65
+ updateStatusColor={update.color}
66
+ />
67
  <main class="flex-grow px-4 md:px-8 lg:px-40 py-8 flex justify-center">
68
  <div class="flex flex-col w-full max-w-[960px] gap-6">
69
  <AddAccount
 
102
  />
103
  </div>
104
  </main>
105
+ <Footer updateStatus={update.status} />
106
  </>
107
  );
108
  }
web/src/components/Footer.tsx CHANGED
@@ -1,12 +1,32 @@
1
  import { useT } from "../../../shared/i18n/context";
 
2
 
3
- export function Footer() {
 
 
 
 
4
  const t = useT();
5
 
 
 
 
 
6
  return (
7
- <footer class="mt-auto border-t border-gray-200 dark:border-border-dark bg-white dark:bg-card-dark py-6 transition-colors">
8
- <div class="container mx-auto px-4 text-center">
9
- <p class="text-[0.8rem] text-slate-500 dark:text-text-dim">{t("footer")}</p>
 
 
 
 
 
 
 
 
 
 
 
10
  </div>
11
  </footer>
12
  );
 
1
  import { useT } from "../../../shared/i18n/context";
2
+ import type { UpdateStatus } from "../../../shared/hooks/use-update-status";
3
 
4
+ interface FooterProps {
5
+ updateStatus: UpdateStatus | null;
6
+ }
7
+
8
+ export function Footer({ updateStatus }: FooterProps) {
9
  const t = useT();
10
 
11
+ const proxyVersion = updateStatus?.proxy.version ?? "...";
12
+ const proxyCommit = updateStatus?.proxy.commit;
13
+ const codexVersion = updateStatus?.codex.current_version;
14
+
15
  return (
16
+ <footer class="mt-auto border-t border-gray-200 dark:border-border-dark bg-white dark:bg-card-dark py-5 transition-colors">
17
+ <div class="container mx-auto px-4 flex flex-col items-center gap-2">
18
+ {/* Version info */}
19
+ <div class="flex flex-wrap items-center justify-center gap-x-3 gap-y-1 text-[0.75rem] text-slate-400 dark:text-text-dim font-mono">
20
+ <span>Proxy v{proxyVersion}{proxyCommit ? ` (${proxyCommit})` : ""}</span>
21
+ {codexVersion && (
22
+ <>
23
+ <span class="text-slate-300 dark:text-border-dark">&middot;</span>
24
+ <span>Codex Desktop v{codexVersion}</span>
25
+ </>
26
+ )}
27
+ </div>
28
+ {/* Copyright */}
29
+ <p class="text-[0.75rem] text-slate-400 dark:text-text-dim">{t("footer")}</p>
30
  </div>
31
  </footer>
32
  );
web/src/components/Header.tsx CHANGED
@@ -15,9 +15,13 @@ const SVG_SUN = (
15
 
16
  interface HeaderProps {
17
  onAddAccount: () => void;
 
 
 
 
18
  }
19
 
20
- export function Header({ onAddAccount }: HeaderProps) {
21
  const { lang, toggleLang, t } = useI18n();
22
  const { isDark, toggle: toggleTheme } = useTheme();
23
 
@@ -46,6 +50,37 @@ export function Header({ onAddAccount }: HeaderProps) {
46
  <span class="col-start-1 row-start-1">{t("serverOnline")}</span>
47
  </span>
48
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
  {/* Language Toggle */}
50
  <button
51
  onClick={toggleLang}
 
15
 
16
  interface HeaderProps {
17
  onAddAccount: () => void;
18
+ onCheckUpdate: () => void;
19
+ checking: boolean;
20
+ updateStatusMsg: string | null;
21
+ updateStatusColor: string;
22
  }
23
 
24
+ export function Header({ onAddAccount, onCheckUpdate, checking, updateStatusMsg, updateStatusColor }: HeaderProps) {
25
  const { lang, toggleLang, t } = useI18n();
26
  const { isDark, toggle: toggleTheme } = useTheme();
27
 
 
50
  <span class="col-start-1 row-start-1">{t("serverOnline")}</span>
51
  </span>
52
  </div>
53
+ {/* Star on GitHub */}
54
+ <a
55
+ href="https://github.com/icebear0828/codex-proxy"
56
+ target="_blank"
57
+ rel="noopener noreferrer"
58
+ class="hidden sm:flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-amber-50 border border-amber-200 text-amber-700 dark:bg-amber-900/20 dark:border-amber-700/30 dark:text-amber-400 hover:bg-amber-100 dark:hover:bg-amber-900/40 transition-colors"
59
+ >
60
+ <svg class="size-3.5" viewBox="0 0 24 24" fill="currentColor">
61
+ <path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" />
62
+ </svg>
63
+ <span class="text-xs font-semibold">{t("starOnGithub")}</span>
64
+ </a>
65
+ {/* Check for Updates */}
66
+ <button
67
+ onClick={onCheckUpdate}
68
+ disabled={checking}
69
+ class="hidden sm:flex items-center gap-1.5 px-3 py-1.5 rounded-full border border-gray-200 dark:border-border-dark text-slate-600 dark:text-text-dim hover:bg-slate-50 dark:hover:bg-border-dark disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
70
+ >
71
+ <svg class={`size-3.5 ${checking ? "animate-spin" : ""}`} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
72
+ <path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.992 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182M20.985 4.356v4.992" />
73
+ </svg>
74
+ <span class="text-xs font-semibold">
75
+ {checking ? t("checkingUpdates") : t("checkForUpdates")}
76
+ </span>
77
+ </button>
78
+ {/* Update status message */}
79
+ {updateStatusMsg && !checking && (
80
+ <span class={`hidden sm:inline text-xs font-medium ${updateStatusColor}`}>
81
+ {updateStatusMsg}
82
+ </span>
83
+ )}
84
  {/* Language Toggle */}
85
  <button
86
  onClick={toggleLang}
web/vite.config.ts CHANGED
@@ -22,6 +22,7 @@ export default defineConfig({
22
  "/auth": "http://localhost:8080",
23
  "/health": "http://localhost:8080",
24
  "/debug": "http://localhost:8080",
 
25
  },
26
  },
27
  });
 
22
  "/auth": "http://localhost:8080",
23
  "/health": "http://localhost:8080",
24
  "/debug": "http://localhost:8080",
25
+ "/admin": "http://localhost:8080",
26
  },
27
  },
28
  });