Replace entire codebase with homarr-labs/homarr
This commit is contained in:
9
apps/tasks/eslint.config.js
Normal file
9
apps/tasks/eslint.config.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import baseConfig from "@homarr/eslint-config/base";
|
||||
|
||||
/** @type {import('typescript-eslint').Config} */
|
||||
export default [
|
||||
{
|
||||
ignores: ["tasks.cjs"],
|
||||
},
|
||||
...baseConfig,
|
||||
];
|
||||
58
apps/tasks/package.json
Normal file
58
apps/tasks/package.json
Normal file
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"name": "@homarr/tasks",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"main": "./src/main.ts",
|
||||
"types": "./src/main.ts",
|
||||
"scripts": {
|
||||
"build": "esbuild src/main.ts --bundle --platform=node --loader:.scss=text --external:*.node --external:@opentelemetry/api --external:deasync --external:bcrypt --outfile=tasks.cjs",
|
||||
"clean": "rm -rf .turbo node_modules",
|
||||
"dev": "pnpm with-env tsx ./src/main.ts",
|
||||
"format": "prettier --check . --ignore-path ../../.gitignore",
|
||||
"lint": "eslint",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"with-env": "dotenv -e ../../.env --"
|
||||
},
|
||||
"prettier": "@homarr/prettier-config",
|
||||
"dependencies": {
|
||||
"@homarr/analytics": "workspace:^0.1.0",
|
||||
"@homarr/auth": "workspace:^0.1.0",
|
||||
"@homarr/common": "workspace:^0.1.0",
|
||||
"@homarr/core": "workspace:^",
|
||||
"@homarr/cron-job-api": "workspace:^0.1.0",
|
||||
"@homarr/cron-jobs": "workspace:^0.1.0",
|
||||
"@homarr/cron-jobs-core": "workspace:^0.1.0",
|
||||
"@homarr/db": "workspace:^0.1.0",
|
||||
"@homarr/definitions": "workspace:^0.1.0",
|
||||
"@homarr/icons": "workspace:^0.1.0",
|
||||
"@homarr/integrations": "workspace:^0.1.0",
|
||||
"@homarr/ping": "workspace:^0.1.0",
|
||||
"@homarr/redis": "workspace:^0.1.0",
|
||||
"@homarr/request-handler": "workspace:^0.1.0",
|
||||
"@homarr/server-settings": "workspace:^0.1.0",
|
||||
"@homarr/validation": "workspace:^0.1.0",
|
||||
"@homarr/widgets": "workspace:^0.1.0",
|
||||
"dayjs": "^1.11.19",
|
||||
"dotenv": "^17.2.3",
|
||||
"fastify": "^5.6.2",
|
||||
"superjson": "2.2.6",
|
||||
"undici": "7.16.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/node": "^24.10.4",
|
||||
"dotenv-cli": "^11.0.0",
|
||||
"esbuild": "^0.27.2",
|
||||
"eslint": "^9.39.2",
|
||||
"prettier": "^3.7.4",
|
||||
"tsx": "4.20.4",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
117
apps/tasks/src/job-manager.ts
Normal file
117
apps/tasks/src/job-manager.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import { schedule, validate as validateCron } from "node-cron";
|
||||
|
||||
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||
import type { IJobManager } from "@homarr/cron-job-api";
|
||||
import type { jobGroup as cronJobGroup, JobGroupKeys } from "@homarr/cron-jobs";
|
||||
import type { Database, InferInsertModel } from "@homarr/db";
|
||||
import { eq } from "@homarr/db";
|
||||
import { cronJobConfigurations } from "@homarr/db/schema";
|
||||
|
||||
const logger = createLogger({ module: "jobManager" });
|
||||
|
||||
export class JobManager implements IJobManager {
|
||||
constructor(
|
||||
private db: Database,
|
||||
private jobGroup: typeof cronJobGroup,
|
||||
) {}
|
||||
|
||||
public async startAsync(name: JobGroupKeys): Promise<void> {
|
||||
await this.jobGroup.startAsync(name);
|
||||
}
|
||||
public async triggerAsync(name: JobGroupKeys): Promise<void> {
|
||||
await this.jobGroup.runManuallyAsync(name);
|
||||
}
|
||||
public async stopAsync(name: JobGroupKeys): Promise<void> {
|
||||
await this.jobGroup.stopAsync(name);
|
||||
}
|
||||
public async updateIntervalAsync(name: JobGroupKeys, cron: string): Promise<void> {
|
||||
logger.info("Updating cron job interval", { name, expression: cron });
|
||||
const job = this.jobGroup.getJobRegistry().get(name);
|
||||
if (!job) throw new Error(`Job ${name} not found`);
|
||||
if (!validateCron(cron)) {
|
||||
throw new Error(`Invalid cron expression: ${cron}`);
|
||||
}
|
||||
await this.updateConfigurationAsync(name, { cronExpression: cron });
|
||||
await this.jobGroup.getTask(name)?.destroy();
|
||||
|
||||
this.jobGroup.setTask(
|
||||
name,
|
||||
schedule(cron, () => void job.executeAsync(), {
|
||||
name,
|
||||
}),
|
||||
);
|
||||
logger.info("Cron job interval updated", { name, expression: cron });
|
||||
}
|
||||
public async disableAsync(name: JobGroupKeys): Promise<void> {
|
||||
logger.info("Disabling cron job", { name });
|
||||
const job = this.jobGroup.getJobRegistry().get(name);
|
||||
if (!job) throw new Error(`Job ${name} not found`);
|
||||
|
||||
await this.updateConfigurationAsync(name, { isEnabled: false });
|
||||
await this.jobGroup.stopAsync(name);
|
||||
logger.info("Cron job disabled", { name });
|
||||
}
|
||||
public async enableAsync(name: JobGroupKeys): Promise<void> {
|
||||
logger.info("Enabling cron job", { name });
|
||||
await this.updateConfigurationAsync(name, { isEnabled: true });
|
||||
await this.jobGroup.startAsync(name);
|
||||
logger.info("Cron job enabled", { name });
|
||||
}
|
||||
|
||||
private async updateConfigurationAsync(
|
||||
name: JobGroupKeys,
|
||||
configuration: Omit<Partial<InferInsertModel<typeof cronJobConfigurations>>, "name">,
|
||||
) {
|
||||
const existingConfig = await this.db.query.cronJobConfigurations.findFirst({
|
||||
where: (table, { eq }) => eq(table.name, name),
|
||||
});
|
||||
|
||||
logger.debug("Updating cron job configuration", {
|
||||
name,
|
||||
configuration: JSON.stringify(configuration),
|
||||
exists: Boolean(existingConfig),
|
||||
});
|
||||
|
||||
if (existingConfig) {
|
||||
await this.db
|
||||
.update(cronJobConfigurations)
|
||||
// prevent updating the name, as it is the primary key
|
||||
.set({ ...configuration, name: undefined })
|
||||
.where(eq(cronJobConfigurations.name, name));
|
||||
logger.debug("Cron job configuration updated", {
|
||||
name,
|
||||
configuration: JSON.stringify(configuration),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const job = this.jobGroup.getJobRegistry().get(name);
|
||||
if (!job) throw new Error(`Job ${name} not found`);
|
||||
|
||||
await this.db.insert(cronJobConfigurations).values({
|
||||
name,
|
||||
cronExpression: configuration.cronExpression ?? job.cronExpression,
|
||||
isEnabled: configuration.isEnabled ?? true,
|
||||
});
|
||||
logger.debug("Cron job configuration updated", {
|
||||
name,
|
||||
configuration: JSON.stringify(configuration),
|
||||
});
|
||||
}
|
||||
|
||||
public async getAllAsync(): Promise<
|
||||
{ name: JobGroupKeys; cron: string; preventManualExecution: boolean; isEnabled: boolean }[]
|
||||
> {
|
||||
const configurations = await this.db.query.cronJobConfigurations.findMany();
|
||||
|
||||
return [...this.jobGroup.getJobRegistry().entries()].map(([name, job]) => {
|
||||
const config = configurations.find((config) => config.name === name);
|
||||
return {
|
||||
name,
|
||||
cron: config?.cronExpression ?? job.cronExpression,
|
||||
preventManualExecution: job.preventManualExecution,
|
||||
isEnabled: config?.isEnabled ?? true,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
52
apps/tasks/src/main.ts
Normal file
52
apps/tasks/src/main.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
// This import has to be the first import in the file so that the agent is overridden before any other modules are imported.
|
||||
import "./overrides";
|
||||
|
||||
import type { FastifyTRPCPluginOptions } from "@trpc/server/adapters/fastify";
|
||||
import { fastifyTRPCPlugin } from "@trpc/server/adapters/fastify";
|
||||
import fastify from "fastify";
|
||||
|
||||
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||
import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error";
|
||||
import type { JobRouter } from "@homarr/cron-job-api";
|
||||
import { jobRouter } from "@homarr/cron-job-api";
|
||||
import { CRON_JOB_API_KEY_HEADER, CRON_JOB_API_PATH, CRON_JOB_API_PORT } from "@homarr/cron-job-api/constants";
|
||||
import { jobGroup } from "@homarr/cron-jobs";
|
||||
import { db } from "@homarr/db";
|
||||
|
||||
import { JobManager } from "./job-manager";
|
||||
import { onStartAsync } from "./on-start";
|
||||
|
||||
const logger = createLogger({ module: "tasksMain" });
|
||||
|
||||
const server = fastify({
|
||||
maxParamLength: 5000,
|
||||
});
|
||||
server.register(fastifyTRPCPlugin, {
|
||||
prefix: CRON_JOB_API_PATH,
|
||||
trpcOptions: {
|
||||
router: jobRouter,
|
||||
createContext: ({ req }) => ({
|
||||
manager: new JobManager(db, jobGroup),
|
||||
apiKey: req.headers[CRON_JOB_API_KEY_HEADER] as string | undefined,
|
||||
}),
|
||||
onError({ path, error }) {
|
||||
logger.error(new ErrorWithMetadata("Error in tasks tRPC handler", { path }, { cause: error }));
|
||||
},
|
||||
} satisfies FastifyTRPCPluginOptions<JobRouter>["trpcOptions"],
|
||||
});
|
||||
|
||||
void (async () => {
|
||||
await onStartAsync();
|
||||
await jobGroup.initializeAsync();
|
||||
await jobGroup.startAllAsync();
|
||||
|
||||
try {
|
||||
await server.listen({ port: CRON_JOB_API_PORT });
|
||||
logger.info("Tasks web server started successfully", { port: CRON_JOB_API_PORT });
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
new ErrorWithMetadata("Failed to start tasks web server", { port: CRON_JOB_API_PORT }, { cause: err }),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
})();
|
||||
7
apps/tasks/src/on-start/index.ts
Normal file
7
apps/tasks/src/on-start/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { invalidateUpdateCheckerCacheAsync } from "./invalidate-update-checker-cache";
|
||||
import { cleanupSessionsAsync } from "./session-cleanup";
|
||||
|
||||
export async function onStartAsync() {
|
||||
await cleanupSessionsAsync();
|
||||
await invalidateUpdateCheckerCacheAsync();
|
||||
}
|
||||
18
apps/tasks/src/on-start/invalidate-update-checker-cache.ts
Normal file
18
apps/tasks/src/on-start/invalidate-update-checker-cache.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||
import { updateCheckerRequestHandler } from "@homarr/request-handler/update-checker";
|
||||
|
||||
const logger = createLogger({ module: "invalidateUpdateCheckerCache" });
|
||||
|
||||
/**
|
||||
* Invalidates the update checker cache on startup to ensure fresh data.
|
||||
* It is important as we want to avoid showing pending updates after the update to latest version.
|
||||
*/
|
||||
export async function invalidateUpdateCheckerCacheAsync() {
|
||||
try {
|
||||
const handler = updateCheckerRequestHandler.handler({});
|
||||
await handler.invalidateAsync();
|
||||
logger.debug("Update checker cache invalidated");
|
||||
} catch (error) {
|
||||
logger.error(new Error("Failed to invalidate update checker cache", { cause: error }));
|
||||
}
|
||||
}
|
||||
41
apps/tasks/src/on-start/session-cleanup.ts
Normal file
41
apps/tasks/src/on-start/session-cleanup.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { env } from "@homarr/auth/env";
|
||||
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||
import { db, eq, inArray } from "@homarr/db";
|
||||
import { sessions, users } from "@homarr/db/schema";
|
||||
import { supportedAuthProviders } from "@homarr/definitions";
|
||||
|
||||
const logger = createLogger({ module: "sessionCleanup" });
|
||||
|
||||
/**
|
||||
* Deletes sessions for users that have inactive auth providers.
|
||||
* Sessions from other providers are deleted so they can no longer be used.
|
||||
*/
|
||||
export async function cleanupSessionsAsync() {
|
||||
try {
|
||||
const currentAuthProviders = env.AUTH_PROVIDERS;
|
||||
|
||||
const inactiveAuthProviders = supportedAuthProviders.filter((provider) => !currentAuthProviders.includes(provider));
|
||||
const subQuery = db
|
||||
.select({ id: users.id })
|
||||
.from(users)
|
||||
.where(inArray(users.provider, inactiveAuthProviders))
|
||||
.as("sq");
|
||||
const sessionsWithInactiveProviders = await db
|
||||
.select({ userId: sessions.userId })
|
||||
.from(sessions)
|
||||
.rightJoin(subQuery, eq(sessions.userId, subQuery.id));
|
||||
|
||||
const userIds = sessionsWithInactiveProviders.map(({ userId }) => userId).filter((value) => value !== null);
|
||||
await db.delete(sessions).where(inArray(sessions.userId, userIds));
|
||||
|
||||
if (sessionsWithInactiveProviders.length > 0) {
|
||||
logger.info("Deleted sessions for inactive providers", {
|
||||
count: userIds.length,
|
||||
});
|
||||
} else {
|
||||
logger.debug("No sessions to delete");
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(new Error("Failed to clean up sessions", { cause: error }));
|
||||
}
|
||||
}
|
||||
5
apps/tasks/src/overrides.ts
Normal file
5
apps/tasks/src/overrides.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { setGlobalDispatcher } from "undici";
|
||||
|
||||
import { UndiciHttpAgent } from "@homarr/core/infrastructure/http";
|
||||
|
||||
setGlobalDispatcher(new UndiciHttpAgent());
|
||||
13
apps/tasks/tsconfig.json
Normal file
13
apps/tasks/tsconfig.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "@homarr/tsconfig/base.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"~/*": ["src/*"],
|
||||
"@homarr/node-unifi": ["../../node_modules/@types/node-unifi"]
|
||||
},
|
||||
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
|
||||
},
|
||||
"include": ["."],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Reference in New Issue
Block a user