feat: add tasks page (#692)

This commit is contained in:
Manuel
2024-07-01 18:57:40 +02:00
committed by GitHub
parent 663eb0bf5b
commit 08d571ad74
43 changed files with 668 additions and 174 deletions

View File

@@ -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",

View File

@@ -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",
},
],
},
{

View File

@@ -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<JobState>(
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 (
<Stack>
{jobs.map((job) => (
<Card key={job.job.name}>
<Group justify={"space-between"} gap={"md"}>
<Stack gap={0}>
<Group>
<Text>{t(`${job.job.name}.label` as TranslationKeys)}</Text>
{job.status?.status === "idle" && <Badge variant="default">{t("status.idle")}</Badge>}
{job.status?.status === "running" && <Badge color="green">{t("status.running")}</Badge>}
{job.status?.lastExecutionStatus === "error" && <Badge color="red">{t("status.error")}</Badge>}
</Group>
{job.status && <TimeAgo timestamp={job.status.lastExecutionTimestamp} />}
</Stack>
<ActionIcon
onClick={() => handleJobTrigger(job)}
disabled={job.status?.status === "running"}
variant={"default"}
size={"xl"}
radius={"xl"}
>
<IconPlayerPlay stroke={1.5} />
</ActionIcon>
</Group>
</Card>
))}
</Stack>
);
};
const TimeAgo = ({ timestamp }: { timestamp: string }) => {
const timeAgo = useTimeAgo(new Date(timestamp));
return (
<Text size={"sm"} c={"dimmed"}>
{timeAgo}
</Text>
);
};

View File

@@ -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 (
<Box>
<Title mb={"md"}>Tasks</Title>
<JobsList initialJobs={jobs} />
</Box>
);
}

View File

@@ -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"

View File

@@ -1,3 +0,0 @@
import { client } from "./queues";
export const queueClient = client;

View File

@@ -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,
});

View File

@@ -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();
});

View File

@@ -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<TInput extends z.ZodType = z.ZodType> {
name: string;
callback: (input: z.infer<TInput>) => MaybePromise<void>;
inputValidator: TInput;
}
type Queues = Record<string, ReturnType<ReturnType<typeof createQueue>["withCallback"]>>;
export const createQueueClient = <TQueues extends Queues>(queues: TQueues) => {
const queueRegistry = new Map<string, Queue>();
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<TQueues[typeof name]["_input"]>, 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<TQueues[key]["_input"]>,
props: {
executionDate?: Date;
} | void,
) => Promise<void>;
},
),
};
};

View File

@@ -1,13 +0,0 @@
import type { MaybePromise } from "@homarr/common/types";
import type { z } from "@homarr/validation";
export const createQueue = <TInput extends z.ZodType>(input: TInput) => {
return {
withCallback: (callback: (data: z.infer<TInput>) => MaybePromise<void>) => {
return {
_input: input,
_callback: callback,
};
},
};
};

View File

@@ -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);
}
};

View File

@@ -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();
})();

View File

@@ -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,
});

View File

@@ -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}`);
});

View File

@@ -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",

View File

@@ -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

View File

@@ -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<TaskStatus>((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");
});
}),
});

View File

@@ -0,0 +1,9 @@
import baseConfig from "@homarr/eslint-config/base";
/** @type {import('typescript-eslint').Config} */
export default [
{
ignores: [],
},
...baseConfig,
];

View File

@@ -0,0 +1 @@
export * from "./src";

View File

@@ -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"
}

View File

@@ -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<JobGroupKeys>("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());

View File

@@ -0,0 +1,8 @@
{
"extends": "@homarr/tsconfig/base.json",
"compilerOptions": {
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
},
"include": ["*.ts", "src"],
"exclude": ["node_modules"]
}

View File

@@ -0,0 +1,9 @@
import baseConfig from "@homarr/eslint-config/base";
/** @type {import('typescript-eslint').Config} */
export default [
{
ignores: [],
},
...baseConfig,
];

View File

@@ -0,0 +1 @@
export * from "./src";

View File

@@ -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"
}

View File

@@ -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<TaskStatus>(`cron-job-status:${name}`);

View File

@@ -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",
});
};

View File

@@ -0,0 +1,8 @@
{
"extends": "@homarr/tsconfig/base.json",
"compilerOptions": {
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
},
"include": ["*.ts", "src"],
"exclude": ["node_modules"]
}

View File

@@ -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 = <TAllowedNames extends string = string>(
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 = <TAllowedNames extends string = string>(
getJobRegistry() {
return jobRegistry as Map<TAllowedNames, ReturnType<JobCallback<TAllowedNames, TAllowedNames>>>;
},
getKeys() {
return objectKeys(jobs);
},
};
};
};

View File

@@ -0,0 +1,9 @@
import baseConfig from "@homarr/eslint-config/base";
/** @type {import('typescript-eslint').Config} */
export default [
{
ignores: [],
},
...baseConfig,
];

View File

@@ -0,0 +1 @@
export * from "./src";

View File

@@ -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"
}

View File

@@ -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];

View File

@@ -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,

View File

@@ -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,

View File

@@ -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({

View File

@@ -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();

View File

@@ -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,
});

View File

@@ -0,0 +1,8 @@
{
"extends": "@homarr/tsconfig/base.json",
"compilerOptions": {
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
},
"include": ["*.ts", "src"],
"exclude": ["node_modules"]
}

View File

@@ -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?`);
}
};

View File

@@ -14,7 +14,7 @@ const lastDataClient = createRedisConnection();
* @param name name of the channel
* @returns pub/sub channel object
*/
export const createSubPubChannel = <TData>(name: string) => {
export const createSubPubChannel = <TData>(name: string, { persist }: { persist: boolean } = { persist: true }) => {
const lastChannelName = `pubSub:last:${name}`;
const channelName = `pubSub:${name}`;
return {
@@ -23,11 +23,13 @@ export const createSubPubChannel = <TData>(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 = <TData>(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<TData>(data) : null;
},
};
};

View File

@@ -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.",

181
pnpm-lock.yaml generated
View File

@@ -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