Add system updates page to admin section
Add a new API endpoint and UI section for checking system updates, including internationalized locale strings and necessary dependency updates. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 1e6de894-7c78-4557-b01a-a2c1c01f4155 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/1WKAcHk Replit-Helium-Checkpoint-Created: true
This commit is contained in:
@@ -35,6 +35,7 @@
|
||||
"pino-http": "^10",
|
||||
"playwright-core": "^1.59.1",
|
||||
"sanitize-html": "^2.17.3",
|
||||
"semver": "^7.6.3",
|
||||
"socket.io": "^4.8.3",
|
||||
"web-push": "^3.6.7",
|
||||
"zod": "catalog:"
|
||||
@@ -50,6 +51,7 @@
|
||||
"@types/nodemailer": "^8.0.0",
|
||||
"@types/pdfkit": "^0.17.6",
|
||||
"@types/sanitize-html": "^2.16.1",
|
||||
"@types/semver": "^7.5.8",
|
||||
"@types/web-push": "^3.6.4",
|
||||
"esbuild": "^0.27.3",
|
||||
"esbuild-plugin-pino": "^2.3.3",
|
||||
|
||||
@@ -17,6 +17,7 @@ import rolesRouter from "./roles";
|
||||
import auditRouter from "./audit";
|
||||
import executiveMeetingsRouter from "./executive-meetings";
|
||||
import pushRouter from "./push";
|
||||
import systemRouter from "./system";
|
||||
|
||||
const router: IRouter = Router();
|
||||
|
||||
@@ -38,5 +39,6 @@ router.use(rolesRouter);
|
||||
router.use(auditRouter);
|
||||
router.use(executiveMeetingsRouter);
|
||||
router.use(pushRouter);
|
||||
router.use(systemRouter);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
import { Router, type IRouter } from "express";
|
||||
import semver from "semver";
|
||||
import { requireAdmin } from "../middlewares/auth";
|
||||
import versionFile from "../../../../version.json" with { type: "json" };
|
||||
import { GetSystemVersionResponse } from "@workspace/api-zod";
|
||||
|
||||
const router: IRouter = Router();
|
||||
|
||||
const CURRENT_VERSION: string = versionFile.version;
|
||||
const CURRENT_BUILD: string = versionFile.build;
|
||||
|
||||
const LATEST_URL = process.env.LATEST_VERSION_URL ?? "";
|
||||
const FETCH_TIMEOUT_MS = 5000;
|
||||
|
||||
type LatestPayload = {
|
||||
version?: unknown;
|
||||
build?: unknown;
|
||||
};
|
||||
|
||||
router.get("/system/version", requireAdmin, async (_req, res): Promise<void> => {
|
||||
const checkedAt = new Date().toISOString();
|
||||
|
||||
if (!LATEST_URL) {
|
||||
res.json(
|
||||
GetSystemVersionResponse.parse({
|
||||
current: { version: CURRENT_VERSION, build: CURRENT_BUILD },
|
||||
latest: null,
|
||||
status: "not-configured",
|
||||
errorMessage: null,
|
||||
checkedAt,
|
||||
configured: false,
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
||||
try {
|
||||
const response = await fetch(LATEST_URL, {
|
||||
signal: controller.signal,
|
||||
headers: { Accept: "application/json" },
|
||||
});
|
||||
if (!response.ok) {
|
||||
res.json(
|
||||
GetSystemVersionResponse.parse({
|
||||
current: { version: CURRENT_VERSION, build: CURRENT_BUILD },
|
||||
latest: null,
|
||||
status: "error",
|
||||
errorMessage: `HTTP ${response.status}`,
|
||||
checkedAt,
|
||||
configured: true,
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const body = (await response.json()) as LatestPayload;
|
||||
const rawVersion = typeof body.version === "string" ? body.version : "";
|
||||
const rawBuild = typeof body.build === "string" ? body.build : "";
|
||||
const cleaned = semver.valid(semver.coerce(rawVersion));
|
||||
const localCleaned = semver.valid(semver.coerce(CURRENT_VERSION));
|
||||
if (!cleaned || !localCleaned) {
|
||||
res.json(
|
||||
GetSystemVersionResponse.parse({
|
||||
current: { version: CURRENT_VERSION, build: CURRENT_BUILD },
|
||||
latest: { version: rawVersion, build: rawBuild },
|
||||
status: "error",
|
||||
errorMessage: "Invalid version format",
|
||||
checkedAt,
|
||||
configured: true,
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
// semver.gt: strictly greater → update available. Equal or older →
|
||||
// up to date (we never tell the user to "downgrade" — the desktop
|
||||
// build is the source of truth for that scenario).
|
||||
const status = semver.gt(cleaned, localCleaned)
|
||||
? "update-available"
|
||||
: "up-to-date";
|
||||
res.json(
|
||||
GetSystemVersionResponse.parse({
|
||||
current: { version: CURRENT_VERSION, build: CURRENT_BUILD },
|
||||
latest: { version: rawVersion, build: rawBuild },
|
||||
status,
|
||||
errorMessage: null,
|
||||
checkedAt,
|
||||
configured: true,
|
||||
}),
|
||||
);
|
||||
} catch (err) {
|
||||
const message =
|
||||
err instanceof Error
|
||||
? err.name === "AbortError"
|
||||
? "Request timed out"
|
||||
: err.message
|
||||
: "Unknown error";
|
||||
res.json(
|
||||
GetSystemVersionResponse.parse({
|
||||
current: { version: CURRENT_VERSION, build: CURRENT_BUILD },
|
||||
latest: null,
|
||||
status: "error",
|
||||
errorMessage: message,
|
||||
checkedAt,
|
||||
configured: true,
|
||||
}),
|
||||
);
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
Reference in New Issue
Block a user