feat: add update indicator (#1626)

This commit is contained in:
Manuel
2024-12-14 18:58:46 +01:00
committed by GitHub
parent cf9656d91e
commit dd9d8b5261
13 changed files with 441 additions and 7 deletions

View File

@@ -13,6 +13,7 @@ import { logRouter } from "./router/log";
import { mediaRouter } from "./router/medias/media-router";
import { searchEngineRouter } from "./router/search-engine/search-engine-router";
import { serverSettingsRouter } from "./router/serverSettings";
import { updateCheckerRouter } from "./router/update-checker";
import { userRouter } from "./router/user";
import { widgetRouter } from "./router/widgets";
import { createTRPCRouter } from "./trpc";
@@ -35,6 +36,7 @@ export const appRouter = createTRPCRouter({
cronJobs: cronJobsRouter,
apiKeys: apiKeysRouter,
media: mediaRouter,
updateChecker: updateCheckerRouter,
});
// export type definition of API

View File

@@ -0,0 +1,11 @@
import { updateCheckerRequestHandler } from "@homarr/request-handler/update-checker";
import { createTRPCRouter, protectedProcedure } from "../trpc";
export const updateCheckerRouter = createTRPCRouter({
getAvailableUpdates: protectedProcedure.query(async () => {
const handler = updateCheckerRequestHandler.handler({});
const data = await handler.getCachedOrUpdatedDataAsync({});
return data.data.availableUpdates;
}),
});

View File

@@ -38,7 +38,8 @@
"@homarr/request-handler": "workspace:^0.1.0",
"@homarr/server-settings": "workspace:^0.1.0",
"@homarr/translation": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0"
"@homarr/validation": "workspace:^0.1.0",
"semver-parser": "^4.1.7"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",

View File

@@ -12,6 +12,7 @@ import { pingJob } from "./jobs/ping";
import type { RssFeed } from "./jobs/rss-feeds";
import { rssFeedsJob } from "./jobs/rss-feeds";
import { sessionCleanupJob } from "./jobs/session-cleanup";
import { updateCheckerJob } from "./jobs/update-checker";
import { createCronJobGroup } from "./lib";
export const jobGroup = createCronJobGroup({
@@ -29,6 +30,7 @@ export const jobGroup = createCronJobGroup({
indexerManager: indexerManagerJob,
healthMonitoring: healthMonitoringJob,
sessionCleanup: sessionCleanupJob,
updateChecker: updateCheckerJob,
});
export type JobGroupKeys = ReturnType<(typeof jobGroup)["getKeys"]>[number];

View File

@@ -0,0 +1,13 @@
import { EVERY_HOUR } from "@homarr/cron-jobs-core/expressions";
import { updateCheckerRequestHandler } from "@homarr/request-handler/update-checker";
import { createCronJob } from "../lib";
export const updateCheckerJob = createCronJob("updateChecker", EVERY_HOUR, {
runOnStart: true,
}).withCallback(async () => {
const handler = updateCheckerRequestHandler.handler({});
await handler.getCachedOrUpdatedDataAsync({
forceUpdate: true,
});
});

View File

@@ -7,6 +7,7 @@ export {
createIntegrationOptionsChannel,
createChannelWithLatestAndEvents,
handshakeAsync,
createSubPubChannel,
} from "./lib/channel";
export const exampleChannel = createSubPubChannel<{ message: string }>("example");

View File

@@ -29,6 +29,7 @@
"@homarr/log": "workspace:^0.1.0",
"@homarr/redis": "workspace:^0.1.0",
"dayjs": "^1.11.13",
"octokit": "^4.0.2",
"pretty-print-error": "^1.1.2",
"superjson": "2.2.2"
},

View File

@@ -0,0 +1,59 @@
import dayjs from "dayjs";
import { Octokit } from "octokit";
import { compareSemVer, isValidSemVer } from "semver-parser";
import { logger } from "@homarr/log";
import { createChannelWithLatestAndEvents } from "@homarr/redis";
import { createCachedRequestHandler } from "@homarr/request-handler/lib/cached-request-handler";
import packageJson from "../../../package.json";
export const updateCheckerRequestHandler = createCachedRequestHandler({
queryKey: "homarr-update-checker",
cacheDuration: dayjs.duration(1, "hour"),
async requestAsync(_) {
const octokit = new Octokit();
const releases = await octokit.rest.repos.listReleases({
owner: "homarr-labs",
repo: "homarr",
});
const currentVersion = (packageJson as { version: string }).version;
const availableReleases = [];
for (const release of releases.data) {
if (!isValidSemVer(release.tag_name)) {
logger.warn(`Unable to parse semantic tag '${release.tag_name}'. Update check might not work.`);
continue;
}
availableReleases.push(release);
}
const availableNewerReleases = availableReleases
.filter((release) => compareSemVer(release.tag_name, currentVersion) > 0)
.sort((releaseA, releaseB) => compareSemVer(releaseB.tag_name, releaseA.tag_name));
if (availableReleases.length > 0) {
logger.info(
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
`Update checker found a new available version: ${availableReleases[0]!.tag_name}. Current version is ${currentVersion}`,
);
} else {
logger.debug(`Update checker did not find any available updates. Current version is ${currentVersion}`);
}
return {
availableUpdates: availableNewerReleases.map((release) => ({
name: release.name,
contentHtml: release.body_html,
url: release.html_url,
tagName: release.tag_name,
})),
};
},
createRedisChannel() {
return createChannelWithLatestAndEvents<{
availableUpdates: { name: string | null; contentHtml?: string; url: string; tagName: string }[];
}>("homarr:update");
},
});

View File

@@ -733,7 +733,8 @@
"logout": "Logout",
"login": "Login",
"homeBoard": "Your home board",
"loggedOut": "Logged out"
"loggedOut": "Logged out",
"updateAvailable": "{countUpdates} updates available: {tag}"
}
},
"dangerZone": "Danger zone",
@@ -2153,6 +2154,9 @@
},
"sessionCleanup": {
"label": "Session Cleanup"
},
"updateChecker": {
"label": "Update checker"
}
}
},