Skip to content

Commit 0d3e1a8

Browse files
authored
fix(gui): auto-retry npm update check on latest_unavailable with Retry button (#77)
- Server: npm view retries up to 3 attempts with backoff (src/update/index.ts) - GUI: auto-retries 2x when latest_unavailable comes back, then shows Retry button - Retry button next to the warning, no dashboard refresh needed - Dead code removed (old UPDATE_CHECK_ATTEMPTS/sleepMs constants) - i18n: dash.updateRetry added for en/ko/zh
1 parent f0681dc commit 0d3e1a8

5 files changed

Lines changed: 48 additions & 5 deletions

File tree

gui/src/i18n/en.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ export const en = {
7979
"dash.updateCommand": "Command",
8080
"dash.updateSource": "This is a source checkout. Update it from the terminal with the shown command.",
8181
"dash.updateUnavailable": "Could not read the latest version from npm. Try again later.",
82+
"dash.updateRetry": "Retry",
8283
"dash.updateRestart": "Restart after update",
8384
"dash.updateRestartHint": "Recommended. The current GUI keeps running the old code until the proxy restarts.",
8485
"dash.runUpdate": "Update",

gui/src/i18n/ko.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ export const ko: Record<TKey, string> = {
7979
"dash.updateCommand": "명령",
8080
"dash.updateSource": "현재는 소스 체크아웃입니다. 표시된 명령을 터미널에서 실행해 업데이트하세요.",
8181
"dash.updateUnavailable": "npm에서 최신 버전을 읽지 못했습니다. 잠시 후 다시 시도하세요.",
82+
"dash.updateRetry": "Retry",
8283
"dash.updateRestart": "업데이트 후 재시작",
8384
"dash.updateRestartHint": "권장. 프록시를 재시작하기 전까지 현재 GUI는 이전 코드로 계속 실행됩니다.",
8485
"dash.runUpdate": "업데이트",

gui/src/i18n/zh.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ export const zh: Record<TKey, string> = {
7979
"dash.updateCommand": "命令",
8080
"dash.updateSource": "当前是源码检出。请在终端运行显示的命令进行更新。",
8181
"dash.updateUnavailable": "无法从 npm 读取最新版本。请稍后重试。",
82+
"dash.updateRetry": "Retry",
8283
"dash.updateRestart": "更新后重启",
8384
"dash.updateRestartHint": "推荐开启。代理重启前,当前 GUI 仍运行旧代码。",
8485
"dash.runUpdate": "更新",

gui/src/pages/Dashboard.tsx

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useEffect, useMemo, useState } from "react";
1+
import { useEffect, useMemo, useRef, useState } from "react";
22
import { formatUptime } from "../formatUptime";
33
import { IconAlert, IconExternal, IconInfo, IconRefresh, IconX } from "../icons";
44
import { useI18n, Trans } from "../i18n";
@@ -98,6 +98,7 @@ export default function Dashboard({ apiBase }: { apiBase: string }) {
9898
const [updateChannel, setUpdateChannel] = useState<UpdateChannel>("latest");
9999
const [updateRestart, setUpdateRestart] = useState(true);
100100
const [updateLoading, setUpdateLoading] = useState(false);
101+
const updateRetryRef = useRef(0);
101102
const [updateCheck, setUpdateCheck] = useState<UpdateCheckData | null>(null);
102103
const [updateError, setUpdateError] = useState<string | null>(null);
103104
const [updateJob, setUpdateJob] = useState<UpdateJob | null>(null);
@@ -317,14 +318,25 @@ export default function Dashboard({ apiBase }: { apiBase: string }) {
317318
const data = await res.json() as UpdateCheckData | { error?: string };
318319
if (!res.ok) throw new Error("error" in data && data.error ? data.error : "update check failed");
319320
setUpdateCheck(data as UpdateCheckData);
321+
updateRetryRef.current = 0;
320322
} catch (err) {
321323
setUpdateError(err instanceof Error ? err.message : String(err));
322324
} finally {
323325
setUpdateLoading(false);
326+
327+
setUpdateCheck(prev => {
328+
if (!prev || prev.reason !== "latest_unavailable") return prev;
329+
const max = 2;
330+
if (updateRetryRef.current >= max) return prev;
331+
updateRetryRef.current += 1;
332+
setTimeout(() => { void fetchUpdateCheck(channel); }, 800 * updateRetryRef.current);
333+
return prev;
334+
});
324335
}
325336
};
326337

327338
const openUpdateDialog = () => {
339+
updateRetryRef.current = 0;
328340
const channel = defaultUpdateChannel(health?.version);
329341
setUpdateChannel(channel);
330342
setUpdateRestart(true);
@@ -333,6 +345,7 @@ export default function Dashboard({ apiBase }: { apiBase: string }) {
333345
};
334346

335347
const changeUpdateChannel = (channel: UpdateChannel) => {
348+
updateRetryRef.current = 0;
336349
setUpdateChannel(channel);
337350
fetchUpdateCheck(channel);
338351
};
@@ -692,7 +705,18 @@ export default function Dashboard({ apiBase }: { apiBase: string }) {
692705
<div className="notice-warn" role="status"><IconAlert /> {t("dash.updateSource")}</div>
693706
)}
694707
{updateCheck.reason === "latest_unavailable" && (
695-
<div className="notice-warn" role="status"><IconAlert /> {t("dash.updateUnavailable")}</div>
708+
<div className="notice-warn" role="status">
709+
<IconAlert /> {t("dash.updateUnavailable")}
710+
<button
711+
type="button"
712+
className="btn btn-ghost btn-sm"
713+
disabled={updateLoading}
714+
onClick={() => { void fetchUpdateCheck(updateChannel); }}
715+
style={{ marginLeft: 12 }}
716+
>
717+
<IconRefresh /> {t("dash.updateRetry")}
718+
</button>
719+
</div>
696720
)}
697721
{updateCheck.canUpdate && (
698722
<div className="spread update-restart">

src/update/index.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,27 @@ function npmSpawnTarget(bin: string): { bin: string; shell: boolean } {
5858
return { bin: "npm.cmd", shell: true };
5959
}
6060

61+
const LATEST_VERSION_ATTEMPTS = 3;
62+
const LATEST_VERSION_RETRY_MS = 500;
63+
64+
function sleepMs(ms: number): void {
65+
const until = Date.now() + ms;
66+
while (Date.now() < until) { /* sync backoff between npm view retries */ }
67+
}
68+
6169
/** Latest published version from the registry (best-effort; null if npm isn't available). */
62-
export function latestVersion(tag: string): string | null {
70+
export function latestVersion(tag: string, options?: { attempts?: number }): string | null {
71+
const attempts = Math.max(1, options?.attempts ?? LATEST_VERSION_ATTEMPTS);
6372
const npm = npmSpawnTarget("npm");
64-
const r = spawnSync(npm.bin, ["view", `${PKG}@${tag}`, "version"], { encoding: "utf8", timeout: 12000, windowsHide: true, shell: npm.shell });
65-
return r.status === 0 ? r.stdout.trim() : null;
73+
for (let attempt = 0; attempt < attempts; attempt++) {
74+
const r = spawnSync(npm.bin, ["view", `${PKG}@${tag}`, "version"], { encoding: "utf8", timeout: 12000, windowsHide: true, shell: npm.shell });
75+
if (r.status === 0) {
76+
const version = r.stdout.trim();
77+
if (version) return version;
78+
}
79+
if (attempt < attempts - 1) sleepMs(LATEST_VERSION_RETRY_MS * (attempt + 1));
80+
}
81+
return null;
6682
}
6783

6884
/** The global-install command opencodex would run to update on this channel. */

0 commit comments

Comments
 (0)