fix: hide stale update failures on status load
This commit is contained in:
+9
-2
@@ -930,6 +930,11 @@ export async function buildApp(database: DatabaseContext, config: AppConfig) {
|
||||
reply.header("Cache-Control", "no-store");
|
||||
reconcileOrphanedUpdateJobs(database.sqlite, config);
|
||||
const cached = publicCheckFromCache(database.sqlite, config);
|
||||
// Status is a live control surface, not an update history endpoint.
|
||||
// Terminal failures/cancellations from a previous attempt must not be
|
||||
// replayed as if the operator had just started an update. They remain in
|
||||
// the database/audit log, while this endpoint exposes only an actionable
|
||||
// task (or the latest successful completion for confirmation).
|
||||
const row = database.sqlite.prepare(`
|
||||
SELECT id, operation, status, version, platform, asset_name AS assetName,
|
||||
size_bytes AS sizeBytes, error_message AS errorMessage,
|
||||
@@ -937,8 +942,10 @@ export async function buildApp(database: DatabaseContext, config: AppConfig) {
|
||||
downloaded_bytes AS downloadedBytes, download_started_at AS downloadStartedAt,
|
||||
download_speed_bps AS downloadSpeedBps,
|
||||
requested_at AS applyQueuedAt
|
||||
FROM update_jobs WHERE admin_id=? ORDER BY created_at DESC LIMIT 1
|
||||
`).get(request.auth!.admin.id) as Record<string, unknown> | undefined;
|
||||
FROM update_jobs
|
||||
WHERE admin_id=? AND status IN (${[...ACTIVE_UPDATE_STATUSES, "completed"].map(() => "?").join(",")})
|
||||
ORDER BY created_at DESC LIMIT 1
|
||||
`).get(request.auth!.admin.id, ...ACTIVE_UPDATE_STATUSES, "completed") as Record<string, unknown> | undefined;
|
||||
return {
|
||||
...cached,
|
||||
strategy: config.updateStrategy,
|
||||
|
||||
@@ -137,6 +137,24 @@ describe("更新 API", () => {
|
||||
expect(disabledConfig.updateStrategy).toBe("disabled");
|
||||
});
|
||||
|
||||
it("首次进入状态页不会展示历史失败任务,也不会阻断新的检查", async () => {
|
||||
const session = await login("update-history");
|
||||
const admin = database.sqlite.prepare("SELECT id FROM admins WHERE username=?").get("update-history") as { id: string };
|
||||
const now = Date.now();
|
||||
database.sqlite.prepare(`
|
||||
INSERT INTO update_jobs(id, admin_id, operation, status, version, platform, asset_url, error_message, created_at, updated_at)
|
||||
VALUES (?, ?, 'download', 'failed', '1.1.0', ?, 'https://updates.example/old.tar.gz', 'old failure', ?, ?)
|
||||
`).run(randomUUID(), admin.id, detectPlatform().target, now - 60_000, now - 60_000);
|
||||
const initial = await app.inject({ method: "GET", url: "/api/update/status", headers: { cookie: session.cookies } });
|
||||
expect(initial.statusCode).toBe(200);
|
||||
expect(initial.json().job).toBeNull();
|
||||
|
||||
mockRelease();
|
||||
const checked = await app.inject({ method: "POST", url: "/api/update/check", headers: { origin: config.publicOrigin, cookie: session.cookies, "x-csrf-token": session.csrf }, payload: {} });
|
||||
expect(checked.statusCode).toBe(200);
|
||||
expect(checked.json().latest).toMatchObject({ version: "1.2.0", isNewer: true });
|
||||
});
|
||||
|
||||
it("更新任务只对发起管理员可见,并隐藏内部错误详情", async () => {
|
||||
const owner = await login("update-owner");
|
||||
const other = await login("update-other");
|
||||
|
||||
@@ -338,7 +338,10 @@ export default function UpdatePage({
|
||||
else setLoading(false);
|
||||
}, [isMock]);
|
||||
|
||||
const job = info?.job;
|
||||
// The status endpoint intentionally hides terminal failures/cancellations.
|
||||
// Keep this guard in the UI as well so a stale response from an older
|
||||
// server cannot turn a fresh page visit into a false failure state.
|
||||
const job = info?.job && info.job.status !== "failed" && info.job.status !== "cancelled" ? info.job : null;
|
||||
useEffect(() => {
|
||||
if (job && activeStatuses.has(job.status)) setShowPipelineDialog(true);
|
||||
}, [job?.id, job?.status]);
|
||||
@@ -441,7 +444,10 @@ export default function UpdatePage({
|
||||
method: "POST",
|
||||
body: "{}",
|
||||
});
|
||||
setLiveInfo((current) => ({ ...result, job: result.job ?? current?.job ?? null }));
|
||||
setLiveInfo((current) => ({
|
||||
...result,
|
||||
job: result.job ?? (current?.job && activeStatuses.has(current.job.status) ? current.job : null),
|
||||
}));
|
||||
notify?.(result.latest?.isNewer ? "发现新版本" : "当前已是最新版本", "success");
|
||||
} catch (caught) {
|
||||
if (caught instanceof ApiError && caught.code === "UPDATE_RATE_LIMITED") {
|
||||
|
||||
Reference in New Issue
Block a user