icebear0828 Claude Opus 4.6 (1M context) commited on
Commit
ca241c4
Β·
1 Parent(s): afe61e7

fix: dark mode tabs, StableText bilingual width, self-update reliability

Browse files

- CodeExamples: fix dark mode tab text invisible (text-dim alpha format
broken with CSS variables, use plain #8b949e; tab background use solid
#0d1117 instead of transparent bg-dark/30)
- StableText: use both en+zh invisible references so button width never
jumps on language switch (root cause of recurring i18n layout bug)
- Header: remove version/commit display (already shown in footer)
- self-update: git checkout -- . before pull to discard local drift;
restart via helper script that waits for port release before starting
new process (fixes EADDRINUSE on Windows)
- apply-update: SSE streaming progress (pull β†’ install β†’ build β†’ restart)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

src/routes/web.ts CHANGED
@@ -1,4 +1,5 @@
1
  import { Hono } from "hono";
 
2
  import { serveStatic } from "@hono/node-server/serve-static";
3
  import { getConnInfo } from "@hono/node-server/conninfo";
4
  import { readFileSync, existsSync } from "fs";
@@ -285,8 +286,21 @@ export function createWebRoutes(accountPool: AccountPool): Hono {
285
  c.status(400);
286
  return c.json({ started: false, error: "Self-update not available in this deploy mode" });
287
  }
288
- const result = await applyProxySelfUpdate();
289
- return c.json(result);
 
 
 
 
 
 
 
 
 
 
 
 
 
290
  });
291
 
292
  // --- Test connection endpoint ---
 
1
  import { Hono } from "hono";
2
+ import { stream } from "hono/streaming";
3
  import { serveStatic } from "@hono/node-server/serve-static";
4
  import { getConnInfo } from "@hono/node-server/conninfo";
5
  import { readFileSync, existsSync } from "fs";
 
286
  c.status(400);
287
  return c.json({ started: false, error: "Self-update not available in this deploy mode" });
288
  }
289
+
290
+ // SSE stream for progress updates
291
+ c.header("Content-Type", "text/event-stream");
292
+ c.header("Cache-Control", "no-cache");
293
+ c.header("Connection", "keep-alive");
294
+
295
+ return stream(c, async (s) => {
296
+ const send = (data: Record<string, unknown>) => s.write(`data: ${JSON.stringify(data)}\n\n`);
297
+
298
+ const result = await applyProxySelfUpdate((step, status, detail) => {
299
+ void send({ step, status, detail });
300
+ });
301
+
302
+ await send({ ...result, done: true });
303
+ });
304
  });
305
 
306
  // --- Test connection endpoint ---
src/self-update.ts CHANGED
@@ -10,6 +10,7 @@ import { existsSync, readFileSync } from "fs";
10
  import { resolve } from "path";
11
  import { promisify } from "util";
12
  import { getRootDir, isEmbedded } from "./paths.js";
 
13
 
14
  // ── Restart ─────────────────────────────────────────────────────────
15
  let _closeHandler: (() => Promise<void>) | null = null;
@@ -20,18 +21,45 @@ export function setCloseHandler(handler: () => Promise<void>): void {
20
  }
21
 
22
  /**
23
- * Restart the server: try graceful close (up to 3s), then spawn new process and exit.
24
- * Works even without a close handler β€” always spawns and exits.
25
  */
26
  function hardRestart(cwd: string): void {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  const doRestart = () => {
28
- console.log("[SelfUpdate] Spawning new process and exiting...");
29
- const child = spawn(process.argv[0], process.argv.slice(1), {
30
  detached: true,
31
  stdio: "ignore",
32
  cwd,
33
- });
34
- child.unref();
35
  process.exit(0);
36
  };
37
 
@@ -56,6 +84,16 @@ function hardRestart(cwd: string): void {
56
  });
57
  }
58
 
 
 
 
 
 
 
 
 
 
 
59
  const execFileAsync = promisify(execFile);
60
 
61
  const GITHUB_REPO = "icebear0828/codex-proxy";
@@ -297,34 +335,47 @@ export async function checkProxySelfUpdate(): Promise<ProxySelfUpdateResult> {
297
  return result;
298
  }
299
 
 
 
 
300
  /**
301
  * Apply proxy self-update: git pull + npm install + npm run build.
302
  * Only works in git (CLI) mode.
 
303
  */
304
- export async function applyProxySelfUpdate(): Promise<{ started: boolean; restarting?: boolean; error?: string }> {
 
 
305
  if (_proxyUpdateInProgress) {
306
  return { started: false, error: "Update already in progress" };
307
  }
308
 
309
  _proxyUpdateInProgress = true;
310
  const cwd = process.cwd();
 
311
 
312
  try {
 
313
  console.log("[SelfUpdate] Pulling latest code...");
314
- // Discard local modifications (e.g. package-lock.json drift) that would block git pull
315
  await execFileAsync("git", ["checkout", "--", "."], { cwd, timeout: 10000 }).catch(() => {});
316
  await execFileAsync("git", ["pull", "origin", "master"], { cwd, timeout: 60000 });
 
317
 
 
318
  console.log("[SelfUpdate] Installing dependencies...");
319
  await execFileAsync("npm", ["install"], { cwd, timeout: 120000, shell: true });
 
320
 
 
321
  console.log("[SelfUpdate] Building...");
322
  await execFileAsync("npm", ["run", "build"], { cwd, timeout: 120000, shell: true });
 
323
 
324
- console.log("[SelfUpdate] Update complete. Restarting in 500ms...");
 
325
  _proxyUpdateInProgress = false;
326
 
327
- // Delay 500ms to let HTTP response flush, then restart
328
  setTimeout(() => hardRestart(cwd), 500);
329
 
330
  return { started: true, restarting: true };
 
10
  import { resolve } from "path";
11
  import { promisify } from "util";
12
  import { getRootDir, isEmbedded } from "./paths.js";
13
+ import { getConfig } from "./config.js";
14
 
15
  // ── Restart ─────────────────────────────────────────────────────────
16
  let _closeHandler: (() => Promise<void>) | null = null;
 
21
  }
22
 
23
  /**
24
+ * Restart the server: try graceful close (up to 3s), then spawn a helper
25
+ * that waits for the port to be free before starting the new server process.
26
  */
27
  function hardRestart(cwd: string): void {
28
+ const nodeExe = process.argv[0];
29
+ const serverArgs = process.argv.slice(1);
30
+
31
+ // Inline script: wait for the port to be free, then start the real server
32
+ const helperScript = `
33
+ const net = require("net");
34
+ const { spawn } = require("child_process");
35
+ const args = ${JSON.stringify(serverArgs)};
36
+ const cwd = ${JSON.stringify(cwd)};
37
+ let attempts = 0;
38
+ function tryStart() {
39
+ attempts++;
40
+ const s = net.createServer();
41
+ s.once("error", () => {
42
+ if (attempts >= 20) process.exit(1);
43
+ setTimeout(tryStart, 500);
44
+ });
45
+ s.once("listening", () => {
46
+ s.close(() => {
47
+ spawn(${JSON.stringify(nodeExe)}, args, { detached: true, stdio: "ignore", cwd }).unref();
48
+ process.exit(0);
49
+ });
50
+ });
51
+ s.listen(${getPort()});
52
+ }
53
+ tryStart();
54
+ `;
55
+
56
  const doRestart = () => {
57
+ console.log("[SelfUpdate] Spawning restart helper and exiting...");
58
+ spawn(nodeExe, ["-e", helperScript], {
59
  detached: true,
60
  stdio: "ignore",
61
  cwd,
62
+ }).unref();
 
63
  process.exit(0);
64
  };
65
 
 
84
  });
85
  }
86
 
87
+ /** Read the configured port for the restart helper. */
88
+ function getPort(): number {
89
+ try {
90
+ const config = getConfig();
91
+ return config.server.port ?? 8080;
92
+ } catch {
93
+ return 8080;
94
+ }
95
+ }
96
+
97
  const execFileAsync = promisify(execFile);
98
 
99
  const GITHUB_REPO = "icebear0828/codex-proxy";
 
335
  return result;
336
  }
337
 
338
+ /** Progress callback for streaming update status. */
339
+ export type UpdateProgressCallback = (step: string, status: "running" | "done" | "error", detail?: string) => void;
340
+
341
  /**
342
  * Apply proxy self-update: git pull + npm install + npm run build.
343
  * Only works in git (CLI) mode.
344
+ * @param onProgress Optional callback to report step-by-step progress.
345
  */
346
+ export async function applyProxySelfUpdate(
347
+ onProgress?: UpdateProgressCallback,
348
+ ): Promise<{ started: boolean; restarting?: boolean; error?: string }> {
349
  if (_proxyUpdateInProgress) {
350
  return { started: false, error: "Update already in progress" };
351
  }
352
 
353
  _proxyUpdateInProgress = true;
354
  const cwd = process.cwd();
355
+ const report = onProgress ?? (() => {});
356
 
357
  try {
358
+ report("pull", "running");
359
  console.log("[SelfUpdate] Pulling latest code...");
 
360
  await execFileAsync("git", ["checkout", "--", "."], { cwd, timeout: 10000 }).catch(() => {});
361
  await execFileAsync("git", ["pull", "origin", "master"], { cwd, timeout: 60000 });
362
+ report("pull", "done");
363
 
364
+ report("install", "running");
365
  console.log("[SelfUpdate] Installing dependencies...");
366
  await execFileAsync("npm", ["install"], { cwd, timeout: 120000, shell: true });
367
+ report("install", "done");
368
 
369
+ report("build", "running");
370
  console.log("[SelfUpdate] Building...");
371
  await execFileAsync("npm", ["run", "build"], { cwd, timeout: 120000, shell: true });
372
+ report("build", "done");
373
 
374
+ report("restart", "running");
375
+ console.log("[SelfUpdate] Update complete. Restarting...");
376
  _proxyUpdateInProgress = false;
377
 
378
+ // Delay 500ms to let SSE flush, then restart
379
  setTimeout(() => hardRestart(cwd), 500);
380
 
381
  return { started: true, restarting: true };
web/src/components/CodeExamples.tsx CHANGED
@@ -190,7 +190,7 @@ export function CodeExamples({ baseUrl, apiKey, model, reasoningEffort, serviceT
190
  <h2 class="text-[0.95rem] font-bold">{t("integrationExamples")}</h2>
191
  <div class="bg-white dark:bg-card-dark border border-gray-200 dark:border-border-dark rounded-xl overflow-hidden shadow-sm transition-colors">
192
  {/* Protocol Tabs */}
193
- <div class="flex border-b border-gray-200 dark:border-border-dark bg-slate-50/50 dark:bg-bg-dark/30">
194
  {protocols.map((p) => (
195
  <button
196
  key={p.id}
 
190
  <h2 class="text-[0.95rem] font-bold">{t("integrationExamples")}</h2>
191
  <div class="bg-white dark:bg-card-dark border border-gray-200 dark:border-border-dark rounded-xl overflow-hidden shadow-sm transition-colors">
192
  {/* Protocol Tabs */}
193
+ <div class="flex border-b border-gray-200 dark:border-border-dark bg-slate-50/50 dark:bg-[#0d1117]">
194
  {protocols.map((p) => (
195
  <button
196
  key={p.id}
web/src/components/Header.tsx CHANGED
@@ -80,12 +80,6 @@ export function Header({ onAddAccount, onCheckUpdate, onOpenUpdateModal, checkin
80
  <span class="relative inline-flex rounded-full h-2.5 w-2.5 bg-primary" />
81
  </span>
82
  <StableText tKey="serverOnline" class="text-xs font-semibold text-primary">{t("serverOnline")}</StableText>
83
- {version && (
84
- <span class="text-[0.65rem] font-mono text-primary/70 whitespace-nowrap">v{version}</span>
85
- )}
86
- {commit && (
87
- <span class="text-[0.6rem] font-mono text-primary/40">{commit.slice(0, 7)}</span>
88
- )}
89
  </div>
90
  {/* Star on GitHub */}
91
  <a
 
80
  <span class="relative inline-flex rounded-full h-2.5 w-2.5 bg-primary" />
81
  </span>
82
  <StableText tKey="serverOnline" class="text-xs font-semibold text-primary">{t("serverOnline")}</StableText>
 
 
 
 
 
 
83
  </div>
84
  {/* Star on GitHub */}
85
  <a
web/tailwind.config.ts CHANGED
@@ -13,7 +13,7 @@ export default {
13
  "card-dark": "var(--card-dark, #161b22)",
14
  "border-dark": "var(--border-dark, #30363d)",
15
  "text-main": "#e6edf3",
16
- "text-dim": "rgb(139 148 158 / <alpha-value>)",
17
  },
18
  fontFamily: {
19
  display: [
 
13
  "card-dark": "var(--card-dark, #161b22)",
14
  "border-dark": "var(--border-dark, #30363d)",
15
  "text-main": "#e6edf3",
16
+ "text-dim": "#8b949e",
17
  },
18
  fontFamily: {
19
  display: [