From 08d571ad74c0c97313664b31bf2d1ce83d91629b Mon Sep 17 00:00:00 2001 From: Manuel <30572287+manuel-rw@users.noreply.github.com> Date: Mon, 1 Jul 2024 18:57:40 +0200 Subject: [PATCH] feat: add tasks page (#692) --- apps/nextjs/package.json | 1 + .../nextjs/src/app/[locale]/manage/layout.tsx | 6 + .../tools/tasks/_components/jobs-list.tsx | 93 +++++++++ .../app/[locale]/manage/tools/tasks/page.tsx | 25 +++ apps/tasks/package.json | 2 + apps/tasks/src/index.ts | 3 - apps/tasks/src/jobs.ts | 17 -- apps/tasks/src/jobs/queue.ts | 9 - apps/tasks/src/lib/queue/client.ts | 54 ------ apps/tasks/src/lib/queue/creator.ts | 13 -- apps/tasks/src/lib/queue/worker.ts | 32 ---- apps/tasks/src/main.ts | 7 +- apps/tasks/src/queues.ts | 7 - apps/tasks/src/queues/test.ts | 11 -- packages/api/package.json | 3 + packages/api/src/root.ts | 2 + packages/api/src/router/cron-jobs.ts | 34 ++++ packages/cron-job-runner/eslint.config.js | 9 + packages/cron-job-runner/index.ts | 1 + packages/cron-job-runner/package.json | 36 ++++ packages/cron-job-runner/src/index.ts | 27 +++ packages/cron-job-runner/tsconfig.json | 8 + packages/cron-job-status/eslint.config.js | 9 + packages/cron-job-status/index.ts | 1 + packages/cron-job-status/package.json | 35 ++++ packages/cron-job-status/src/index.ts | 10 + packages/cron-job-status/src/publisher.ts | 38 ++++ packages/cron-job-status/tsconfig.json | 8 + packages/cron-jobs-core/src/group.ts | 12 +- packages/cron-jobs/eslint.config.js | 9 + packages/cron-jobs/index.ts | 1 + packages/cron-jobs/package.json | 45 +++++ packages/cron-jobs/src/index.ts | 14 ++ .../cron-jobs}/src/jobs/analytics.ts | 4 +- .../cron-jobs}/src/jobs/icons-updater.ts | 2 +- .../src/jobs/integrations/home-assistant.ts | 5 +- .../cron-jobs}/src/jobs/ping.ts | 2 +- .../cron-jobs/src/lib/index.ts | 9 +- packages/cron-jobs/tsconfig.json | 8 + packages/integrations/src/base/creator.ts | 2 +- packages/redis/src/lib/channel.ts | 22 ++- packages/translation/src/lang/en.ts | 25 +++ pnpm-lock.yaml | 181 +++++++++++++++++- 43 files changed, 668 insertions(+), 174 deletions(-) create mode 100644 apps/nextjs/src/app/[locale]/manage/tools/tasks/_components/jobs-list.tsx create mode 100644 apps/nextjs/src/app/[locale]/manage/tools/tasks/page.tsx delete mode 100644 apps/tasks/src/index.ts delete mode 100644 apps/tasks/src/jobs.ts delete mode 100644 apps/tasks/src/jobs/queue.ts delete mode 100644 apps/tasks/src/lib/queue/client.ts delete mode 100644 apps/tasks/src/lib/queue/creator.ts delete mode 100644 apps/tasks/src/lib/queue/worker.ts delete mode 100644 apps/tasks/src/queues.ts delete mode 100644 apps/tasks/src/queues/test.ts create mode 100644 packages/api/src/router/cron-jobs.ts create mode 100644 packages/cron-job-runner/eslint.config.js create mode 100644 packages/cron-job-runner/index.ts create mode 100644 packages/cron-job-runner/package.json create mode 100644 packages/cron-job-runner/src/index.ts create mode 100644 packages/cron-job-runner/tsconfig.json create mode 100644 packages/cron-job-status/eslint.config.js create mode 100644 packages/cron-job-status/index.ts create mode 100644 packages/cron-job-status/package.json create mode 100644 packages/cron-job-status/src/index.ts create mode 100644 packages/cron-job-status/src/publisher.ts create mode 100644 packages/cron-job-status/tsconfig.json create mode 100644 packages/cron-jobs/eslint.config.js create mode 100644 packages/cron-jobs/index.ts create mode 100644 packages/cron-jobs/package.json create mode 100644 packages/cron-jobs/src/index.ts rename {apps/tasks => packages/cron-jobs}/src/jobs/analytics.ts (85%) rename {apps/tasks => packages/cron-jobs}/src/jobs/icons-updater.ts (98%) rename {apps/tasks => packages/cron-jobs}/src/jobs/integrations/home-assistant.ts (91%) rename {apps/tasks => packages/cron-jobs}/src/jobs/ping.ts (94%) rename apps/tasks/src/lib/jobs.ts => packages/cron-jobs/src/lib/index.ts (55%) create mode 100644 packages/cron-jobs/tsconfig.json diff --git a/apps/nextjs/package.json b/apps/nextjs/package.json index 907f8c5d6..b88e01703 100644 --- a/apps/nextjs/package.json +++ b/apps/nextjs/package.json @@ -18,6 +18,7 @@ "@homarr/api": "workspace:^0.1.0", "@homarr/auth": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0", + "@homarr/cron-job-status": "workspace:^0.1.0", "@homarr/db": "workspace:^0.1.0", "@homarr/definitions": "workspace:^0.1.0", "@homarr/form": "workspace:^0.1.0", diff --git a/apps/nextjs/src/app/[locale]/manage/layout.tsx b/apps/nextjs/src/app/[locale]/manage/layout.tsx index 63e964b29..bcb5990e0 100644 --- a/apps/nextjs/src/app/[locale]/manage/layout.tsx +++ b/apps/nextjs/src/app/[locale]/manage/layout.tsx @@ -14,6 +14,7 @@ import { IconMailForward, IconPlug, IconQuestionMark, + IconReport, IconSettings, IconTool, IconUser, @@ -86,6 +87,11 @@ export default async function ManageLayout({ children }: PropsWithChildren) { icon: IconLogs, href: "/manage/tools/logs", }, + { + label: t("items.tools.items.tasks"), + icon: IconReport, + href: "/manage/tools/tasks", + }, ], }, { diff --git a/apps/nextjs/src/app/[locale]/manage/tools/tasks/_components/jobs-list.tsx b/apps/nextjs/src/app/[locale]/manage/tools/tasks/_components/jobs-list.tsx new file mode 100644 index 000000000..9ebbba9c1 --- /dev/null +++ b/apps/nextjs/src/app/[locale]/manage/tools/tasks/_components/jobs-list.tsx @@ -0,0 +1,93 @@ +"use client"; + +import React from "react"; +import { ActionIcon, Badge, Card, Group, Stack, Text } from "@mantine/core"; +import { useListState } from "@mantine/hooks"; +import { IconPlayerPlay } from "@tabler/icons-react"; + +import type { RouterOutputs } from "@homarr/api"; +import { clientApi } from "@homarr/api/client"; +import { useTimeAgo } from "@homarr/common"; +import type { TaskStatus } from "@homarr/cron-job-status"; +import type { TranslationKeys } from "@homarr/translation"; +import { useScopedI18n } from "@homarr/translation/client"; + +interface JobsListProps { + initialJobs: RouterOutputs["cronJobs"]["getJobs"]; +} + +interface JobState { + job: JobsListProps["initialJobs"][number]; + status: TaskStatus | null; +} + +export const JobsList = ({ initialJobs }: JobsListProps) => { + const t = useScopedI18n("management.page.tool.tasks"); + const [jobs, handlers] = useListState( + initialJobs.map((job) => ({ + job, + status: null, + })), + ); + clientApi.cronJobs.subscribeToStatusUpdates.useSubscription(undefined, { + onData: (data) => { + const jobByName = jobs.find((job) => job.job.name === data.name); + if (!jobByName) { + return; + } + handlers.applyWhere( + (job) => job.job.name === data.name, + (job) => ({ ...job, status: data }), + ); + }, + }); + const { mutateAsync } = clientApi.cronJobs.triggerJob.useMutation(); + const handleJobTrigger = React.useCallback( + async (job: JobState) => { + if (job.status?.status === "running") { + return; + } + await mutateAsync(job.job.name); + }, + [mutateAsync], + ); + return ( + + {jobs.map((job) => ( + + + + + {t(`${job.job.name}.label` as TranslationKeys)} + {job.status?.status === "idle" && {t("status.idle")}} + {job.status?.status === "running" && {t("status.running")}} + {job.status?.lastExecutionStatus === "error" && {t("status.error")}} + + {job.status && } + + + handleJobTrigger(job)} + disabled={job.status?.status === "running"} + variant={"default"} + size={"xl"} + radius={"xl"} + > + + + + + ))} + + ); +}; + +const TimeAgo = ({ timestamp }: { timestamp: string }) => { + const timeAgo = useTimeAgo(new Date(timestamp)); + + return ( + + {timeAgo} + + ); +}; diff --git a/apps/nextjs/src/app/[locale]/manage/tools/tasks/page.tsx b/apps/nextjs/src/app/[locale]/manage/tools/tasks/page.tsx new file mode 100644 index 000000000..834935659 --- /dev/null +++ b/apps/nextjs/src/app/[locale]/manage/tools/tasks/page.tsx @@ -0,0 +1,25 @@ +import { Box, Title } from "@mantine/core"; + +import { api } from "@homarr/api/server"; +import { getScopedI18n } from "@homarr/translation/server"; + +import { createMetaTitle } from "~/metadata"; +import { JobsList } from "./_components/jobs-list"; + +export async function generateMetadata() { + const t = await getScopedI18n("management"); + + return { + title: createMetaTitle(t("metaTitle")), + }; +} + +export default async function TasksPage() { + const jobs = await api.cronJobs.getJobs(); + return ( + + Tasks + + + ); +} diff --git a/apps/tasks/package.json b/apps/tasks/package.json index 44c6b2bd9..ef410161a 100644 --- a/apps/tasks/package.json +++ b/apps/tasks/package.json @@ -32,6 +32,8 @@ "@homarr/validation": "workspace:^0.1.0", "@homarr/analytics": "workspace:^0.1.0", "@homarr/cron-jobs-core": "workspace:^0.1.0", + "@homarr/cron-jobs": "workspace:^0.1.0", + "@homarr/cron-job-runner": "workspace:^0.1.0", "dotenv": "^16.4.5", "superjson": "2.2.1", "undici": "6.19.2" diff --git a/apps/tasks/src/index.ts b/apps/tasks/src/index.ts deleted file mode 100644 index 48079af2d..000000000 --- a/apps/tasks/src/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { client } from "./queues"; - -export const queueClient = client; diff --git a/apps/tasks/src/jobs.ts b/apps/tasks/src/jobs.ts deleted file mode 100644 index 88cbb3ac3..000000000 --- a/apps/tasks/src/jobs.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { iconsUpdaterJob } from "~/jobs/icons-updater"; -import { smartHomeEntityStateJob } from "~/jobs/integrations/home-assistant"; -import { analyticsJob } from "./jobs/analytics"; -import { pingJob } from "./jobs/ping"; -import { queuesJob } from "./jobs/queue"; -import { createCronJobGroup } from "./lib/jobs"; - -export const jobs = createCronJobGroup({ - // Add your jobs here: - analytics: analyticsJob, - iconsUpdater: iconsUpdaterJob, - ping: pingJob, - smartHomeEntityState: smartHomeEntityStateJob, - - // This job is used to process queues. - queues: queuesJob, -}); diff --git a/apps/tasks/src/jobs/queue.ts b/apps/tasks/src/jobs/queue.ts deleted file mode 100644 index a0fc50bb0..000000000 --- a/apps/tasks/src/jobs/queue.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { EVERY_MINUTE } from "@homarr/cron-jobs-core/expressions"; - -import { createCronJob } from "~/lib/jobs"; -import { queueWorkerAsync } from "../lib/queue/worker"; - -// This job processes queues, it runs every minute. -export const queuesJob = createCronJob("queues", EVERY_MINUTE).withCallback(async () => { - await queueWorkerAsync(); -}); diff --git a/apps/tasks/src/lib/queue/client.ts b/apps/tasks/src/lib/queue/client.ts deleted file mode 100644 index 379fcdbcf..000000000 --- a/apps/tasks/src/lib/queue/client.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { objectEntries, objectKeys } from "@homarr/common"; -import type { MaybePromise } from "@homarr/common/types"; -import { queueChannel } from "@homarr/redis"; -import type { z } from "@homarr/validation"; - -import type { createQueue } from "./creator"; - -interface Queue { - name: string; - callback: (input: z.infer) => MaybePromise; - inputValidator: TInput; -} - -type Queues = Record["withCallback"]>>; - -export const createQueueClient = (queues: TQueues) => { - const queueRegistry = new Map(); - for (const [name, queue] of objectEntries(queues)) { - if (typeof name !== "string") continue; - queueRegistry.set(name, { - name, - callback: queue._callback, - inputValidator: queue._input, - }); - } - - return { - queueRegistry, - client: objectKeys(queues).reduce( - (acc, name) => { - acc[name] = async (data: z.infer, options) => { - if (typeof name !== "string") return; - const queue = queueRegistry.get(name); - if (!queue) return; - - await queueChannel.addAsync({ - name, - data, - executionDate: typeof options === "object" && options.executionDate ? options.executionDate : new Date(), - }); - }; - return acc; - }, - {} as { - [key in keyof TQueues]: ( - data: z.infer, - props: { - executionDate?: Date; - } | void, - ) => Promise; - }, - ), - }; -}; diff --git a/apps/tasks/src/lib/queue/creator.ts b/apps/tasks/src/lib/queue/creator.ts deleted file mode 100644 index 438276f63..000000000 --- a/apps/tasks/src/lib/queue/creator.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { MaybePromise } from "@homarr/common/types"; -import type { z } from "@homarr/validation"; - -export const createQueue = (input: TInput) => { - return { - withCallback: (callback: (data: z.infer) => MaybePromise) => { - return { - _input: input, - _callback: callback, - }; - }, - }; -}; diff --git a/apps/tasks/src/lib/queue/worker.ts b/apps/tasks/src/lib/queue/worker.ts deleted file mode 100644 index 0e7577794..000000000 --- a/apps/tasks/src/lib/queue/worker.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { logger } from "@homarr/log"; -import { queueChannel } from "@homarr/redis"; - -import { queueRegistry } from "~/queues"; - -/** - * This function reads all the queue executions that are due and processes them. - * Those executions are stored in the redis queue channel. - */ -export const queueWorkerAsync = async () => { - const now = new Date(); - const executions = await queueChannel.filterAsync((item) => { - return item.executionDate < now; - }); - for (const execution of executions) { - const queue = queueRegistry.get(execution.name); - if (!queue) continue; - - try { - await queue.callback(execution.data); - } catch (err) { - logger.error( - `apps/tasks/src/lib/queue/worker.ts: Error occured when executing queue ${execution.name} with data`, - execution.data, - "and error:", - err, - ); - } - - await queueChannel.markAsDoneAsync(execution._id); - } -}; diff --git a/apps/tasks/src/main.ts b/apps/tasks/src/main.ts index c876da2da..4ae066828 100644 --- a/apps/tasks/src/main.ts +++ b/apps/tasks/src/main.ts @@ -1,10 +1,13 @@ // This import has to be the first import in the file so that the agent is overridden before any other modules are imported. import "./undici-log-agent-override"; -import { jobs } from "./jobs"; +import { registerCronJobRunner } from "@homarr/cron-job-runner"; +import { jobGroup } from "@homarr/cron-jobs"; + import { seedServerSettingsAsync } from "./seed-server-settings"; void (async () => { - await jobs.startAllAsync(); + registerCronJobRunner(); + await jobGroup.startAllAsync(); await seedServerSettingsAsync(); })(); diff --git a/apps/tasks/src/queues.ts b/apps/tasks/src/queues.ts deleted file mode 100644 index 18eb5bf62..000000000 --- a/apps/tasks/src/queues.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { createQueueClient } from "./lib/queue/client"; -import { testQueue } from "./queues/test"; - -export const { client, queueRegistry } = createQueueClient({ - // Add your queues here - test: testQueue, -}); diff --git a/apps/tasks/src/queues/test.ts b/apps/tasks/src/queues/test.ts deleted file mode 100644 index f6d7019bd..000000000 --- a/apps/tasks/src/queues/test.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { z } from "@homarr/validation"; - -import { createQueue } from "~/lib/queue/creator"; - -export const testQueue = createQueue( - z.object({ - id: z.string(), - }), -).withCallback(({ id }) => { - console.log(`Test queue with id ${id}`); -}); diff --git a/packages/api/package.json b/packages/api/package.json index 3f4aadf2f..35dddb5f0 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -21,6 +21,9 @@ "dependencies": { "@homarr/auth": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0", + "@homarr/cron-jobs": "workspace:^0.1.0", + "@homarr/cron-job-runner": "workspace:^0.1.0", + "@homarr/cron-job-status": "workspace:^0.1.0", "@homarr/db": "workspace:^0.1.0", "@homarr/definitions": "workspace:^0.1.0", "@homarr/integrations": "workspace:^0.1.0", diff --git a/packages/api/src/root.ts b/packages/api/src/root.ts index ce71e53b8..33eaf7780 100644 --- a/packages/api/src/root.ts +++ b/packages/api/src/root.ts @@ -1,5 +1,6 @@ import { appRouter as innerAppRouter } from "./router/app"; import { boardRouter } from "./router/board"; +import { cronJobsRouter } from "./router/cron-jobs"; import { dockerRouter } from "./router/docker/docker-router"; import { groupRouter } from "./router/group"; import { homeRouter } from "./router/home"; @@ -27,6 +28,7 @@ export const appRouter = createTRPCRouter({ home: homeRouter, docker: dockerRouter, serverSettings: serverSettingsRouter, + cronJobs: cronJobsRouter, }); // export type definition of API diff --git a/packages/api/src/router/cron-jobs.ts b/packages/api/src/router/cron-jobs.ts new file mode 100644 index 000000000..598f25d5f --- /dev/null +++ b/packages/api/src/router/cron-jobs.ts @@ -0,0 +1,34 @@ +import { observable } from "@trpc/server/observable"; + +import { jobNameSchema, triggerCronJobAsync } from "@homarr/cron-job-runner"; +import type { TaskStatus } from "@homarr/cron-job-status"; +import { createCronJobStatusChannel } from "@homarr/cron-job-status"; +import { jobGroup } from "@homarr/cron-jobs"; +import { logger } from "@homarr/log"; + +import { createTRPCRouter, publicProcedure } from "../trpc"; + +export const cronJobsRouter = createTRPCRouter({ + triggerJob: publicProcedure.input(jobNameSchema).mutation(async ({ input }) => { + await triggerCronJobAsync(input); + }), + getJobs: publicProcedure.query(() => { + const registry = jobGroup.getJobRegistry(); + return [...registry.values()].map((job) => ({ + name: job.name, + expression: job.cronExpression, + })); + }), + subscribeToStatusUpdates: publicProcedure.subscription(() => { + return observable((emit) => { + for (const job of jobGroup.getJobRegistry().values()) { + const channel = createCronJobStatusChannel(job.name); + channel.subscribe((data) => { + emit.next(data); + }); + } + + logger.info("A tRPC client has connected to the cron job status updates procedure"); + }); + }), +}); diff --git a/packages/cron-job-runner/eslint.config.js b/packages/cron-job-runner/eslint.config.js new file mode 100644 index 000000000..5b19b6f8a --- /dev/null +++ b/packages/cron-job-runner/eslint.config.js @@ -0,0 +1,9 @@ +import baseConfig from "@homarr/eslint-config/base"; + +/** @type {import('typescript-eslint').Config} */ +export default [ + { + ignores: [], + }, + ...baseConfig, +]; diff --git a/packages/cron-job-runner/index.ts b/packages/cron-job-runner/index.ts new file mode 100644 index 000000000..3bd16e178 --- /dev/null +++ b/packages/cron-job-runner/index.ts @@ -0,0 +1 @@ +export * from "./src"; diff --git a/packages/cron-job-runner/package.json b/packages/cron-job-runner/package.json new file mode 100644 index 000000000..6c7c31c11 --- /dev/null +++ b/packages/cron-job-runner/package.json @@ -0,0 +1,36 @@ +{ + "name": "@homarr/cron-job-runner", + "private": true, + "version": "0.1.0", + "type": "module", + "exports": { + ".": "./index.ts" + }, + "typesVersions": { + "*": { + "*": [ + "src/*" + ] + } + }, + "license": "MIT", + "scripts": { + "clean": "rm -rf .turbo node_modules", + "lint": "eslint", + "format": "prettier --check . --ignore-path ../../.gitignore", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@homarr/cron-jobs": "workspace:^0.1.0", + "@homarr/log": "workspace:^0.1.0", + "@homarr/redis": "workspace:^0.1.0" + }, + "devDependencies": { + "@homarr/eslint-config": "workspace:^0.2.0", + "@homarr/prettier-config": "workspace:^0.1.0", + "@homarr/tsconfig": "workspace:^0.1.0", + "eslint": "^9.6.0", + "typescript": "^5.5.2" + }, + "prettier": "@homarr/prettier-config" +} diff --git a/packages/cron-job-runner/src/index.ts b/packages/cron-job-runner/src/index.ts new file mode 100644 index 000000000..caf0c185b --- /dev/null +++ b/packages/cron-job-runner/src/index.ts @@ -0,0 +1,27 @@ +import type { JobGroupKeys } from "@homarr/cron-jobs"; +import { jobGroup } from "@homarr/cron-jobs"; + +import { createSubPubChannel } from "../../redis/src/lib/channel"; +import { zodEnumFromArray } from "../../validation/src/enums"; + +const cronJobRunnerChannel = createSubPubChannel("cron-job-runner", { persist: false }); + +/** + * Registers the cron job runner to listen to the Redis PubSub channel. + */ +export const registerCronJobRunner = () => { + cronJobRunnerChannel.subscribe((jobName) => { + jobGroup.runManually(jobName); + }); +}; + +/** + * Triggers a cron job to run immediately. + * This works over the Redis PubSub channel. + * @param jobName name of the job to be triggered + */ +export const triggerCronJobAsync = async (jobName: JobGroupKeys) => { + await cronJobRunnerChannel.publishAsync(jobName); +}; + +export const jobNameSchema = zodEnumFromArray(jobGroup.getKeys()); diff --git a/packages/cron-job-runner/tsconfig.json b/packages/cron-job-runner/tsconfig.json new file mode 100644 index 000000000..cbe8483d9 --- /dev/null +++ b/packages/cron-job-runner/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "@homarr/tsconfig/base.json", + "compilerOptions": { + "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json" + }, + "include": ["*.ts", "src"], + "exclude": ["node_modules"] +} diff --git a/packages/cron-job-status/eslint.config.js b/packages/cron-job-status/eslint.config.js new file mode 100644 index 000000000..5b19b6f8a --- /dev/null +++ b/packages/cron-job-status/eslint.config.js @@ -0,0 +1,9 @@ +import baseConfig from "@homarr/eslint-config/base"; + +/** @type {import('typescript-eslint').Config} */ +export default [ + { + ignores: [], + }, + ...baseConfig, +]; diff --git a/packages/cron-job-status/index.ts b/packages/cron-job-status/index.ts new file mode 100644 index 000000000..3bd16e178 --- /dev/null +++ b/packages/cron-job-status/index.ts @@ -0,0 +1 @@ +export * from "./src"; diff --git a/packages/cron-job-status/package.json b/packages/cron-job-status/package.json new file mode 100644 index 000000000..441e2f1c0 --- /dev/null +++ b/packages/cron-job-status/package.json @@ -0,0 +1,35 @@ +{ + "name": "@homarr/cron-job-status", + "private": true, + "version": "0.1.0", + "type": "module", + "exports": { + ".": "./index.ts", + "./publisher": "./src/publisher.ts" + }, + "typesVersions": { + "*": { + "*": [ + "src/*" + ] + } + }, + "license": "MIT", + "scripts": { + "clean": "rm -rf .turbo node_modules", + "lint": "eslint", + "format": "prettier --check . --ignore-path ../../.gitignore", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@homarr/redis": "workspace:^0.1.0" + }, + "devDependencies": { + "@homarr/eslint-config": "workspace:^0.2.0", + "@homarr/prettier-config": "workspace:^0.1.0", + "@homarr/tsconfig": "workspace:^0.1.0", + "eslint": "^9.6.0", + "typescript": "^5.5.2" + }, + "prettier": "@homarr/prettier-config" +} diff --git a/packages/cron-job-status/src/index.ts b/packages/cron-job-status/src/index.ts new file mode 100644 index 000000000..14af29e2f --- /dev/null +++ b/packages/cron-job-status/src/index.ts @@ -0,0 +1,10 @@ +import { createSubPubChannel } from "../../redis/src/lib/channel"; + +export interface TaskStatus { + name: string; + status: "running" | "idle"; + lastExecutionTimestamp: string; + lastExecutionStatus: "success" | "error" | null; +} + +export const createCronJobStatusChannel = (name: string) => createSubPubChannel(`cron-job-status:${name}`); diff --git a/packages/cron-job-status/src/publisher.ts b/packages/cron-job-status/src/publisher.ts new file mode 100644 index 000000000..538429d50 --- /dev/null +++ b/packages/cron-job-status/src/publisher.ts @@ -0,0 +1,38 @@ +import { createCronJobStatusChannel } from "."; + +export const beforeCallbackAsync = async (name: string) => { + const channel = createCronJobStatusChannel(name); + + const previous = await channel.getLastDataAsync(); + + await channel.publishAsync({ + name, + lastExecutionStatus: previous?.lastExecutionStatus ?? null, + lastExecutionTimestamp: new Date().toISOString(), + status: "running", + }); +}; + +export const onCallbackSuccessAsync = async (name: string) => { + const channel = createCronJobStatusChannel(name); + + const previous = await channel.getLastDataAsync(); + await channel.publishAsync({ + name, + lastExecutionStatus: "success", + lastExecutionTimestamp: previous?.lastExecutionTimestamp ?? new Date().toISOString(), + status: "idle", + }); +}; + +export const onCallbackErrorAsync = async (name: string, _error: unknown) => { + const channel = createCronJobStatusChannel(name); + + const previous = await channel.getLastDataAsync(); + await channel.publishAsync({ + name, + lastExecutionStatus: "error", + lastExecutionTimestamp: previous?.lastExecutionTimestamp ?? new Date().toISOString(), + status: "idle", + }); +}; diff --git a/packages/cron-job-status/tsconfig.json b/packages/cron-job-status/tsconfig.json new file mode 100644 index 000000000..cbe8483d9 --- /dev/null +++ b/packages/cron-job-status/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "@homarr/tsconfig/base.json", + "compilerOptions": { + "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json" + }, + "include": ["*.ts", "src"], + "exclude": ["node_modules"] +} diff --git a/packages/cron-jobs-core/src/group.ts b/packages/cron-jobs-core/src/group.ts index 772c4cce8..76dd1c765 100644 --- a/packages/cron-jobs-core/src/group.ts +++ b/packages/cron-jobs-core/src/group.ts @@ -1,4 +1,4 @@ -import { objectEntries } from "@homarr/common"; +import { objectEntries, objectKeys } from "@homarr/common"; import type { JobCallback } from "./creator"; import type { Logger } from "./logger"; @@ -43,6 +43,13 @@ export const createJobGroupCreator = ( job.scheduledTask.start(); } }, + runManually: (name: keyof TJobs) => { + const job = jobRegistry.get(name as string); + if (!job) return; + + options.logger.logInfo(`Running schedule cron job ${job.name} manually.`); + job.scheduledTask.now(); + }, stop: (name: keyof TJobs) => { const job = jobRegistry.get(name as string); if (!job) return; @@ -59,6 +66,9 @@ export const createJobGroupCreator = ( getJobRegistry() { return jobRegistry as Map>>; }, + getKeys() { + return objectKeys(jobs); + }, }; }; }; diff --git a/packages/cron-jobs/eslint.config.js b/packages/cron-jobs/eslint.config.js new file mode 100644 index 000000000..5b19b6f8a --- /dev/null +++ b/packages/cron-jobs/eslint.config.js @@ -0,0 +1,9 @@ +import baseConfig from "@homarr/eslint-config/base"; + +/** @type {import('typescript-eslint').Config} */ +export default [ + { + ignores: [], + }, + ...baseConfig, +]; diff --git a/packages/cron-jobs/index.ts b/packages/cron-jobs/index.ts new file mode 100644 index 000000000..3bd16e178 --- /dev/null +++ b/packages/cron-jobs/index.ts @@ -0,0 +1 @@ +export * from "./src"; diff --git a/packages/cron-jobs/package.json b/packages/cron-jobs/package.json new file mode 100644 index 000000000..b3d67732b --- /dev/null +++ b/packages/cron-jobs/package.json @@ -0,0 +1,45 @@ +{ + "name": "@homarr/cron-jobs", + "private": true, + "version": "0.1.0", + "type": "module", + "exports": { + ".": "./index.ts" + }, + "typesVersions": { + "*": { + "*": [ + "src/*" + ] + } + }, + "license": "MIT", + "scripts": { + "clean": "rm -rf .turbo node_modules", + "lint": "eslint", + "format": "prettier --check . --ignore-path ../../.gitignore", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@homarr/log": "workspace:^0.1.0", + "@homarr/cron-job-status": "workspace:^0.1.0", + "@homarr/cron-jobs-core": "workspace:^0.1.0", + "@homarr/analytics": "workspace:^0.1.0", + "@homarr/db": "workspace:^0.1.0", + "@homarr/server-settings": "workspace:^0.1.0", + "@homarr/common": "workspace:^0.1.0", + "@homarr/icons": "workspace:^0.1.0", + "@homarr/integrations": "workspace:^0.1.0", + "@homarr/redis": "workspace:^0.1.0", + "@homarr/ping": "workspace:^0.1.0", + "@homarr/translation": "workspace:^0.1.0" + }, + "devDependencies": { + "@homarr/eslint-config": "workspace:^0.2.0", + "@homarr/prettier-config": "workspace:^0.1.0", + "@homarr/tsconfig": "workspace:^0.1.0", + "eslint": "^9.6.0", + "typescript": "^5.5.2" + }, + "prettier": "@homarr/prettier-config" +} diff --git a/packages/cron-jobs/src/index.ts b/packages/cron-jobs/src/index.ts new file mode 100644 index 000000000..c16bc32f7 --- /dev/null +++ b/packages/cron-jobs/src/index.ts @@ -0,0 +1,14 @@ +import { analyticsJob } from "./jobs/analytics"; +import { iconsUpdaterJob } from "./jobs/icons-updater"; +import { smartHomeEntityStateJob } from "./jobs/integrations/home-assistant"; +import { pingJob } from "./jobs/ping"; +import { createCronJobGroup } from "./lib"; + +export const jobGroup = createCronJobGroup({ + analytics: analyticsJob, + iconsUpdater: iconsUpdaterJob, + ping: pingJob, + smartHomeEntityState: smartHomeEntityStateJob, +}); + +export type JobGroupKeys = ReturnType<(typeof jobGroup)["getKeys"]>[number]; diff --git a/apps/tasks/src/jobs/analytics.ts b/packages/cron-jobs/src/jobs/analytics.ts similarity index 85% rename from apps/tasks/src/jobs/analytics.ts rename to packages/cron-jobs/src/jobs/analytics.ts index b20cb8a52..fc68622a1 100644 --- a/apps/tasks/src/jobs/analytics.ts +++ b/packages/cron-jobs/src/jobs/analytics.ts @@ -4,9 +4,9 @@ import { sendServerAnalyticsAsync } from "@homarr/analytics"; import { EVERY_WEEK } from "@homarr/cron-jobs-core/expressions"; import { db, eq } from "@homarr/db"; import { serverSettings } from "@homarr/db/schema/sqlite"; +import type { defaultServerSettings } from "@homarr/server-settings"; -import { createCronJob } from "~/lib/jobs"; -import type { defaultServerSettings } from "../../../../packages/server-settings"; +import { createCronJob } from "../lib"; export const analyticsJob = createCronJob("analytics", EVERY_WEEK, { runOnStart: true, diff --git a/apps/tasks/src/jobs/icons-updater.ts b/packages/cron-jobs/src/jobs/icons-updater.ts similarity index 98% rename from apps/tasks/src/jobs/icons-updater.ts rename to packages/cron-jobs/src/jobs/icons-updater.ts index 1bc109098..71cd7f2ee 100644 --- a/apps/tasks/src/jobs/icons-updater.ts +++ b/packages/cron-jobs/src/jobs/icons-updater.ts @@ -7,7 +7,7 @@ import { iconRepositories, icons } from "@homarr/db/schema/sqlite"; import { fetchIconsAsync } from "@homarr/icons"; import { logger } from "@homarr/log"; -import { createCronJob } from "~/lib/jobs"; +import { createCronJob } from "../lib"; export const iconsUpdaterJob = createCronJob("iconsUpdater", EVERY_WEEK, { runOnStart: true, diff --git a/apps/tasks/src/jobs/integrations/home-assistant.ts b/packages/cron-jobs/src/jobs/integrations/home-assistant.ts similarity index 91% rename from apps/tasks/src/jobs/integrations/home-assistant.ts rename to packages/cron-jobs/src/jobs/integrations/home-assistant.ts index d3ac3e040..ddb1919b1 100644 --- a/apps/tasks/src/jobs/integrations/home-assistant.ts +++ b/packages/cron-jobs/src/jobs/integrations/home-assistant.ts @@ -7,9 +7,10 @@ import { items } from "@homarr/db/schema/sqlite"; import { HomeAssistantIntegration } from "@homarr/integrations"; import { logger } from "@homarr/log"; import { homeAssistantEntityState } from "@homarr/redis"; -import type { WidgetComponentProps } from "@homarr/widgets"; -import { createCronJob } from "~/lib/jobs"; +// This import is done that way to avoid circular dependencies. +import type { WidgetComponentProps } from "../../../../widgets"; +import { createCronJob } from "../../lib"; export const smartHomeEntityStateJob = createCronJob("smartHomeEntityState", EVERY_MINUTE).withCallback(async () => { const itemsForIntegration = await db.query.items.findMany({ diff --git a/apps/tasks/src/jobs/ping.ts b/packages/cron-jobs/src/jobs/ping.ts similarity index 94% rename from apps/tasks/src/jobs/ping.ts rename to packages/cron-jobs/src/jobs/ping.ts index 90b7b96c0..e9b353112 100644 --- a/apps/tasks/src/jobs/ping.ts +++ b/packages/cron-jobs/src/jobs/ping.ts @@ -3,7 +3,7 @@ import { logger } from "@homarr/log"; import { sendPingRequestAsync } from "@homarr/ping"; import { pingChannel, pingUrlChannel } from "@homarr/redis"; -import { createCronJob } from "~/lib/jobs"; +import { createCronJob } from "../lib"; export const pingJob = createCronJob("ping", EVERY_MINUTE).withCallback(async () => { const urls = await pingUrlChannel.getAllAsync(); diff --git a/apps/tasks/src/lib/jobs.ts b/packages/cron-jobs/src/lib/index.ts similarity index 55% rename from apps/tasks/src/lib/jobs.ts rename to packages/cron-jobs/src/lib/index.ts index 7cebfc85e..b0bc4f243 100644 --- a/apps/tasks/src/lib/jobs.ts +++ b/packages/cron-jobs/src/lib/index.ts @@ -1,6 +1,8 @@ +import { beforeCallbackAsync, onCallbackErrorAsync, onCallbackSuccessAsync } from "@homarr/cron-job-status/publisher"; import { createCronJobFunctions } from "@homarr/cron-jobs-core"; import type { Logger } from "@homarr/cron-jobs-core/logger"; import { logger } from "@homarr/log"; +import type { TranslationObject } from "@homarr/translation"; class WinstonCronJobLogger implements Logger { logDebug(message: string) { @@ -16,6 +18,11 @@ class WinstonCronJobLogger implements Logger { } } -export const { createCronJob, createCronJobGroup } = createCronJobFunctions({ +export const { createCronJob, createCronJobGroup } = createCronJobFunctions< + keyof TranslationObject["management"]["page"]["tool"]["tasks"]["job"] +>({ logger: new WinstonCronJobLogger(), + beforeCallback: beforeCallbackAsync, + onCallbackSuccess: onCallbackSuccessAsync, + onCallbackError: onCallbackErrorAsync, }); diff --git a/packages/cron-jobs/tsconfig.json b/packages/cron-jobs/tsconfig.json new file mode 100644 index 000000000..cbe8483d9 --- /dev/null +++ b/packages/cron-jobs/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "@homarr/tsconfig/base.json", + "compilerOptions": { + "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json" + }, + "include": ["*.ts", "src"], + "exclude": ["node_modules"] +} diff --git a/packages/integrations/src/base/creator.ts b/packages/integrations/src/base/creator.ts index 50aee2be8..3730ee063 100644 --- a/packages/integrations/src/base/creator.ts +++ b/packages/integrations/src/base/creator.ts @@ -11,6 +11,6 @@ export const integrationCreatorByKind = (kind: IntegrationKind, integration: Int case "homeAssistant": return new HomeAssistantIntegration(integration); default: - throw new Error(`Unknown integration kind ${kind}`); + throw new Error(`Unknown integration kind ${kind}. Did you forget to add it to the integration creator?`); } }; diff --git a/packages/redis/src/lib/channel.ts b/packages/redis/src/lib/channel.ts index 7a59e6fd7..08288e5c7 100644 --- a/packages/redis/src/lib/channel.ts +++ b/packages/redis/src/lib/channel.ts @@ -14,7 +14,7 @@ const lastDataClient = createRedisConnection(); * @param name name of the channel * @returns pub/sub channel object */ -export const createSubPubChannel = (name: string) => { +export const createSubPubChannel = (name: string, { persist }: { persist: boolean } = { persist: true }) => { const lastChannelName = `pubSub:last:${name}`; const channelName = `pubSub:${name}`; return { @@ -23,11 +23,13 @@ export const createSubPubChannel = (name: string) => { * @param callback callback function to be called when new data is published */ subscribe: (callback: (data: TData) => void) => { - void lastDataClient.get(lastChannelName).then((data) => { - if (data) { - callback(superjson.parse(data)); - } - }); + if (persist) { + void lastDataClient.get(lastChannelName).then((data) => { + if (data) { + callback(superjson.parse(data)); + } + }); + } void subscriber.subscribe(channelName, (err) => { if (!err) { return; @@ -45,9 +47,15 @@ export const createSubPubChannel = (name: string) => { * @param data data to be published */ publishAsync: async (data: TData) => { - await lastDataClient.set(lastChannelName, superjson.stringify(data)); + if (persist) { + await lastDataClient.set(lastChannelName, superjson.stringify(data)); + } await publisher.publish(channelName, superjson.stringify(data)); }, + getLastDataAsync: async () => { + const data = await lastDataClient.get(lastChannelName); + return data ? superjson.parse(data) : null; + }, }; }; diff --git a/packages/translation/src/lang/en.ts b/packages/translation/src/lang/en.ts index 98071e128..83b5d6ec2 100644 --- a/packages/translation/src/lang/en.ts +++ b/packages/translation/src/lang/en.ts @@ -1252,6 +1252,7 @@ export default { items: { docker: "Docker", logs: "Logs", + tasks: "Tasks", }, }, settings: "Settings", @@ -1451,6 +1452,30 @@ export default { }, }, }, + tool: { + tasks: { + title: "Tasks", + status: { + idle: "Idle", + running: "Running", + error: "Error", + }, + job: { + iconsUpdater: { + label: "Icons Updater", + }, + analytics: { + label: "Analytics", + }, + smartHomeEntityState: { + label: "Smart Home Entity State", + }, + ping: { + label: "Pings", + }, + }, + }, + }, about: { version: "Version {version}", text: "Homarr is a community driven open source project that is being maintained by volunteers. Thanks to these people, Homarr has been a growing project since 2021. Our team is working completely remote from many different countries on Homarr in their leisure time for no compensation.", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 278d11a68..cc0802bb3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -62,6 +62,9 @@ importers: '@homarr/common': specifier: workspace:^0.1.0 version: link:../../packages/common + '@homarr/cron-job-status': + specifier: workspace:^0.1.0 + version: link:../../packages/cron-job-status '@homarr/db': specifier: workspace:^0.1.0 version: link:../../packages/db @@ -258,6 +261,12 @@ importers: '@homarr/common': specifier: workspace:^0.1.0 version: link:../../packages/common + '@homarr/cron-job-runner': + specifier: workspace:^0.1.0 + version: link:../../packages/cron-job-runner + '@homarr/cron-jobs': + specifier: workspace:^0.1.0 + version: link:../../packages/cron-jobs '@homarr/cron-jobs-core': specifier: workspace:^0.1.0 version: link:../../packages/cron-jobs-core @@ -429,6 +438,15 @@ importers: '@homarr/common': specifier: workspace:^0.1.0 version: link:../common + '@homarr/cron-job-runner': + specifier: workspace:^0.1.0 + version: link:../cron-job-runner + '@homarr/cron-job-status': + specifier: workspace:^0.1.0 + version: link:../cron-job-status + '@homarr/cron-jobs': + specifier: workspace:^0.1.0 + version: link:../cron-jobs '@homarr/db': specifier: workspace:^0.1.0 version: link:../db @@ -589,6 +607,111 @@ importers: specifier: ^5.5.2 version: 5.5.2 + packages/cron-job-runner: + dependencies: + '@homarr/cron-jobs': + specifier: workspace:^0.1.0 + version: link:../cron-jobs + '@homarr/log': + specifier: workspace:^0.1.0 + version: link:../log + '@homarr/redis': + specifier: workspace:^0.1.0 + version: link:../redis + devDependencies: + '@homarr/eslint-config': + specifier: workspace:^0.2.0 + version: link:../../tooling/eslint + '@homarr/prettier-config': + specifier: workspace:^0.1.0 + version: link:../../tooling/prettier + '@homarr/tsconfig': + specifier: workspace:^0.1.0 + version: link:../../tooling/typescript + eslint: + specifier: ^9.6.0 + version: 9.6.0 + typescript: + specifier: ^5.5.2 + version: 5.5.2 + + packages/cron-job-status: + dependencies: + '@homarr/redis': + specifier: workspace:^0.1.0 + version: link:../redis + devDependencies: + '@homarr/eslint-config': + specifier: workspace:^0.2.0 + version: link:../../tooling/eslint + '@homarr/prettier-config': + specifier: workspace:^0.1.0 + version: link:../../tooling/prettier + '@homarr/tsconfig': + specifier: workspace:^0.1.0 + version: link:../../tooling/typescript + eslint: + specifier: ^9.6.0 + version: 9.6.0 + typescript: + specifier: ^5.5.2 + version: 5.5.2 + + packages/cron-jobs: + dependencies: + '@homarr/analytics': + specifier: workspace:^0.1.0 + version: link:../analytics + '@homarr/common': + specifier: workspace:^0.1.0 + version: link:../common + '@homarr/cron-job-status': + specifier: workspace:^0.1.0 + version: link:../cron-job-status + '@homarr/cron-jobs-core': + specifier: workspace:^0.1.0 + version: link:../cron-jobs-core + '@homarr/db': + specifier: workspace:^0.1.0 + version: link:../db + '@homarr/icons': + specifier: workspace:^0.1.0 + version: link:../icons + '@homarr/integrations': + specifier: workspace:^0.1.0 + version: link:../integrations + '@homarr/log': + specifier: workspace:^0.1.0 + version: link:../log + '@homarr/ping': + specifier: workspace:^0.1.0 + version: link:../ping + '@homarr/redis': + specifier: workspace:^0.1.0 + version: link:../redis + '@homarr/server-settings': + specifier: workspace:^0.1.0 + version: link:../server-settings + '@homarr/translation': + specifier: workspace:^0.1.0 + version: link:../translation + devDependencies: + '@homarr/eslint-config': + specifier: workspace:^0.2.0 + version: link:../../tooling/eslint + '@homarr/prettier-config': + specifier: workspace:^0.1.0 + version: link:../../tooling/prettier + '@homarr/tsconfig': + specifier: workspace:^0.1.0 + version: link:../../tooling/typescript + eslint: + specifier: ^9.6.0 + version: 9.6.0 + typescript: + specifier: ^5.5.2 + version: 5.5.2 + packages/cron-jobs-core: dependencies: '@homarr/common': @@ -1380,10 +1503,18 @@ packages: resolution: {integrity: sha512-CvLSkwXGWnYlF9+J3iZUvwgAxKiYzK3BWuo+mLzD/MDGOZDj7Gq8+hqaOkMxmJwmlv0iu86uH5fdADd9Hxkymw==} engines: {node: '>=6.9.0'} + '@babel/helper-string-parser@7.23.4': + resolution: {integrity: sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==} + engines: {node: '>=6.9.0'} + '@babel/helper-string-parser@7.24.6': resolution: {integrity: sha512-WdJjwMEkmBicq5T9fm/cHND3+UlFa2Yj8ALLgmoSQAJZysYbBjw+azChSGPN4DSPLXOcooGRvDwZWMcF/mLO2Q==} engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.22.20': + resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==} + engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.24.6': resolution: {integrity: sha512-4yA7s865JHaqUdRbnaxarZREuPTHrjpDT+pXoAZ1yhyo6uFnIEpS8VMu16siFOHDpZNKYv5BObhsB//ycbICyw==} engines: {node: '>=6.9.0'} @@ -1400,6 +1531,11 @@ packages: resolution: {integrity: sha512-2YnuOp4HAk2BsBrJJvYCbItHx0zWscI1C3zgWkz+wDyD9I7GIVrfnLyrR4Y1VR+7p+chAEcrgRQYZAGIKMV7vQ==} engines: {node: '>=6.9.0'} + '@babel/parser@7.24.0': + resolution: {integrity: sha512-QuP/FxEAzMSjXygs8v4N9dvdXzEHN4W1oF3PxuWAtPo08UdM17u89RDMgjLn/mlc56iM0HlLmVkO/wgR+rDgHg==} + engines: {node: '>=6.0.0'} + hasBin: true + '@babel/parser@7.24.6': resolution: {integrity: sha512-eNZXdfU35nJC2h24RznROuOpO94h6x8sg9ju0tT9biNtLZ2vuP8SduLqqV+/8+cebSLV9SJEAN5Z3zQbJG/M+Q==} engines: {node: '>=6.0.0'} @@ -1433,6 +1569,10 @@ packages: resolution: {integrity: sha512-OsNjaJwT9Zn8ozxcfoBc+RaHdj3gFmCmYoQLUII1o6ZrUwku0BMg80FoOTPx+Gi6XhcQxAYE4xyjPTo4SxEQqw==} engines: {node: '>=6.9.0'} + '@babel/types@7.24.0': + resolution: {integrity: sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==} + engines: {node: '>=6.9.0'} + '@babel/types@7.24.6': resolution: {integrity: sha512-WaMsgi6Q8zMgMth93GvWPXkhAIEobfsIkLTacoVZoK1J0CevIPGYY2Vo5YvJGqyHqXM6P4ppOYGsIRU8MM9pFQ==} engines: {node: '>=6.9.0'} @@ -2900,6 +3040,11 @@ packages: resolution: {integrity: sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==} engines: {node: '>=0.4.0'} + acorn@8.11.3: + resolution: {integrity: sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==} + engines: {node: '>=0.4.0'} + hasBin: true + acorn@8.12.0: resolution: {integrity: sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==} engines: {node: '>=0.4.0'} @@ -6545,7 +6690,7 @@ snapshots: '@babel/traverse': 7.24.6 '@babel/types': 7.24.6 convert-source-map: 2.0.0 - debug: 4.3.5 + debug: 4.3.4 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -6601,8 +6746,12 @@ snapshots: dependencies: '@babel/types': 7.24.6 + '@babel/helper-string-parser@7.23.4': {} + '@babel/helper-string-parser@7.24.6': {} + '@babel/helper-validator-identifier@7.22.20': {} + '@babel/helper-validator-identifier@7.24.6': {} '@babel/helper-validator-option@7.24.6': {} @@ -6619,6 +6768,10 @@ snapshots: js-tokens: 4.0.0 picocolors: 1.0.0 + '@babel/parser@7.24.0': + dependencies: + '@babel/types': 7.24.6 + '@babel/parser@7.24.6': dependencies: '@babel/types': 7.24.6 @@ -6663,6 +6816,12 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/types@7.24.0': + dependencies: + '@babel/helper-string-parser': 7.23.4 + '@babel/helper-validator-identifier': 7.22.20 + to-fast-properties: 2.0.0 + '@babel/types@7.24.6': dependencies: '@babel/helper-string-parser': 7.24.6 @@ -8050,6 +8209,8 @@ snapshots: acorn-walk@8.3.2: {} + acorn@8.11.3: {} + acorn@8.12.0: {} aes-decrypter@4.0.1: @@ -8777,7 +8938,7 @@ snapshots: docker-modem@5.0.3: dependencies: - debug: 4.3.5 + debug: 4.3.4 readable-stream: 3.6.2 split-ca: 1.0.1 ssh2: 1.15.0 @@ -9027,7 +9188,7 @@ snapshots: esbuild-register@3.5.0(esbuild@0.19.12): dependencies: - debug: 4.3.5 + debug: 4.3.4 esbuild: 0.19.12 transitivePeerDependencies: - supports-color @@ -9640,7 +9801,7 @@ snapshots: http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.0 - debug: 4.3.5 + debug: 4.3.4 transitivePeerDependencies: - supports-color @@ -9654,7 +9815,7 @@ snapshots: https-proxy-agent@7.0.4: dependencies: agent-base: 7.1.0 - debug: 4.3.5 + debug: 4.3.4 transitivePeerDependencies: - supports-color @@ -9927,7 +10088,7 @@ snapshots: istanbul-lib-source-maps@5.0.4: dependencies: '@jridgewell/trace-mapping': 0.3.25 - debug: 4.3.5 + debug: 4.3.4 istanbul-lib-coverage: 3.2.2 transitivePeerDependencies: - supports-color @@ -10169,8 +10330,8 @@ snapshots: magicast@0.3.3: dependencies: - '@babel/parser': 7.24.6 - '@babel/types': 7.24.6 + '@babel/parser': 7.24.0 + '@babel/types': 7.24.0 source-map-js: 1.2.0 make-dir@3.1.0: @@ -10270,7 +10431,7 @@ snapshots: mlly@1.5.0: dependencies: - acorn: 8.12.0 + acorn: 8.11.3 pathe: 1.1.2 pkg-types: 1.0.3 ufo: 1.4.0 @@ -11617,7 +11778,7 @@ snapshots: '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 '@types/node': 20.14.9 - acorn: 8.12.0 + acorn: 8.11.3 acorn-walk: 8.3.2 arg: 4.1.3 create-require: 1.1.1