Compare commits

...
Author SHA1 Message Date
Thomas Camlong 03f0d12b32 feat: report container RAM usage to PostHog every 30 minutes
Adds a background loop in run.sh that reads cgroup v2 memory stats
and sends a `ram_usage` event to PostHog EU. This lets us track
real-world memory footprint across releases.

Configurable via RAM_REPORT_INTERVAL env var (defaults to 1800s).
2026-05-07 19:02:00 +02:00
Releases Homarr c83fdb2a6e chore(release): 1.60.1-beta.2 [skip ci] 2026-05-07 16:39:41 +00:00
homarr-releases[bot] 81fdd90e7a chore(release): release v1.60.1-beta 2026-05-07 16:38:37 +00:00
Thomas 5ee84f8cf4 perf: (next branch) reduce idle memory on startup (#5637) 2026-05-06 21:39:26 +00:00
Releases Homarr 80d18bb10d chore(release): 1.60.1-beta.1 [skip ci] 2026-05-01 20:44:34 +00:00
homarr-releases[bot] 2bb0f61da7 chore(release): release v1.60.1-beta 2026-05-01 20:43:32 +00:00
Meier Lukas 24386656fd Merge branch 'next' of https://github.com/homarr-labs/homarr into next 2026-05-01 22:42:07 +02:00
Meier Lukas 453360fe61 ci: disable package-manager cache 2026-05-01 22:42:02 +02:00
Releases Homarr beac1ed780 chore(release): 1.60.1-beta.1 [skip ci] 2026-05-01 20:41:02 +00:00
homarr-releases[bot] aa1b3810f1 chore(release): release v1.60.1-beta 2026-05-01 20:40:05 +00:00
Meier Lukas ce3c1b6cc1 chore: retrigger release v1.60.1-beta.1 2026-05-01 22:38:50 +02:00
Releases Homarr 3f6b830f9b chore(release): 1.60.1-beta.1 [skip ci] 2026-05-01 20:36:08 +00:00
homarr-releases[bot] f0cb79799a chore(release): release v1.60.1-beta 2026-05-01 20:35:08 +00:00
Meier Lukas 7561462594 Merge branch 'main' into next 2026-05-01 22:30:33 +02:00
Meier LukasandCopilot bfa8566284 ci: add beta release workflow (#5601)
Co-authored-by: Copilot <copilot@github.com>
2026-05-01 22:18:45 +02:00
Meier LukasandCopilot a1aa5505a3 perf(memory): combine 3 node processes into one (#5600)
Co-authored-by: Copilot <copilot@github.com>
2026-05-01 16:35:11 +02:00
Meier LukasandCopilot e77b9bb3d2 ci: support releases on next branch (#5599)
Co-authored-by: Copilot <copilot@github.com>
2026-04-30 18:28:32 +02:00
26 changed files with 250 additions and 357 deletions

No files matched your search

-4
View File
@@ -12,10 +12,6 @@ AUTH_SECRET="supersecret"
# or starting the project without any (which will show a randomly generated one).
SECRET_ENCRYPTION_KEY=0000000000000000000000000000000000000000000000000000000000000000
# Cron job API key is generated every time the container starts as it is required for communication between nextjs-api and tasks-api
# export CRON_JOB_API_KEY=$(openssl rand -base64 32)
# CRON_JOB_API_KEY="your-generated-api-key"
LOG_LEVEL='info'
# This is how you can use the sqlite driver:
@@ -0,0 +1,104 @@
name: "[Deployment] Beta Release"
on:
workflow_dispatch:
inputs:
send-notifications:
type: boolean
required: false
default: true
description: Send notifications
permissions:
contents: write
pull-requests: write
jobs:
create-and-merge-pr:
runs-on: ubuntu-latest
timeout-minutes: 2
steps:
- name: Discord notification
if: ${{ github.event.inputs.send-notifications }}
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@0.4.0
with:
args: "Beta release has been triggered: [run ${{ github.run_number }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})"
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Get Next Version
id: semver
uses: ietf-tools/semver-action@v1
with:
token: ${{ github.token }}
branch: next
- name: Create pull request
id: create-pull-request
run: 'gh pr create --title "chore(release): release ${{ steps.semver.outputs.next }}-beta" --body "Detected change to be ``${{ steps.semver.outputs.bump }}``<br/>Bump version from ``${{ steps.semver.outputs.current }}`` to ``${{ steps.semver.outputs.next }}-beta``" --base beta --head next --label automerge'
env:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
- name: Discord notification
if: ${{ github.event.inputs.send-notifications }}
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@0.4.0
with:
args: "Created a release PR ${{ steps.create-pull-request.outputs.url }} for version ${{ steps.semver.outputs.next }}-beta (new behaviour: ${{ steps.semver.outputs.bump }})"
- name: Obtain token
id: obtainApprovalToken
uses: actions/create-github-app-token@v3
with:
private-key: ${{ secrets.RENOVATE_APPROVE_PRIVATE_KEY }}
client-id: ${{ secrets.RENOVATE_APPROVE_APP_ID }}
permission-pull-requests: write
- name: Approve PR
env:
GITHUB_TOKEN: ${{ steps.obtainApprovalToken.outputs.token }}
run: |
gh pr review --approve --body "Automatically approved by GitHub Action"
- name: Obtain token
id: obtainMergeToken
uses: actions/create-github-app-token@v3
with:
private-key: ${{ secrets.RENOVATE_MERGE_PRIVATE_KEY }}
client-id: ${{ secrets.RENOVATE_MERGE_APP_ID }}
permission-contents: write # write to beta branch (due to merge)
permission-pull-requests: write # merge pull request
- id: automerge
if: ${{ steps.semver.outputs.bump != 'major' }}
name: automerge
uses: "pascalgn/automerge-action@v0.16.4"
env:
GITHUB_TOKEN: ${{ steps.obtainMergeToken.outputs.token }}
MERGE_METHOD: merge # we prefer merge commits for merging to beta
MERGE_COMMIT_MESSAGE: "chore(release): release ${{ steps.semver.outputs.next }}-beta"
MERGE_DELETE_BRANCH: false # never set to true!
PULL_REQUEST: "${{ steps.create-pull-request.outputs.pr_number }}"
MERGE_RETRIES: 20 # 20 retries * MERGE_RETRY_SLEEP until step fails
MERGE_RETRY_SLEEP: 10000 # 10 seconds * MERGE_RETRIES until step fails
MERGE_REQUIRED_APPROVALS: 0 # do not require approvals
- name: Merged Discord notification
if: ${{ steps.automerge.outputs.mergeResult == 'merged' && github.event.inputs.send-notifications }}
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@0.4.0
with:
args: "Merged PR ${{ steps.create-pull-request.outputs.url }} for release ${{ steps.semver.outputs.next }}-beta"
- name: Major Bump Discord notification
if: ${{ steps.semver.outputs.bump == 'major' && github.event.inputs.send-notifications }}
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@0.4.0
with:
args: "The release PR must be manually merged because the next version is a major version: ${{ steps.create-pull-request.outputs.url }} for release ${{ steps.semver.outputs.next }}-beta"
- name: Discord Fail Notification
if: failure() && github.event.inputs.send-notifications
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@0.4.0
with:
args: "The beta release workflow [run ${{ github.run_number }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) has failed"
+10 -8
View File
@@ -5,6 +5,7 @@ on:
branches:
- main
- beta
- next
- dev
workflow_dispatch:
inputs:
@@ -34,7 +35,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 5
env:
SKIP_RELEASE: ${{ github.event_name == 'workflow_dispatch' || github.ref_name == 'dev' }}
SKIP_RELEASE: ${{ github.event_name == 'workflow_dispatch' || github.ref_name == 'dev' || github.ref_name == 'next' }}
outputs:
version: ${{ steps.read-semver.outputs.version || steps.version-fallback.outputs.version }}
git_ref: ${{ steps.read-git-ref.outputs.ref || github.ref }}
@@ -54,7 +55,7 @@ jobs:
with:
private-key: ${{ secrets.RENOVATE_MERGE_PRIVATE_KEY }}
client-id: ${{ secrets.RENOVATE_MERGE_APP_ID }}
permission-contents: write # required to commit package.json & changelog changes, merge them to dev and publish the release
permission-contents: write # required to commit package.json & changelog changes, merge them to dev / next and publish the release
- uses: actions/checkout@v6
if: env.SKIP_RELEASE == 'false'
@@ -66,7 +67,7 @@ jobs:
if: env.SKIP_RELEASE == 'false'
with:
node-version: 24.15.0
cache: "pnpm"
package-manager-cache: false
- run: npm i -g pnpm
if: env.SKIP_RELEASE == 'false'
- name: Install dependencies
@@ -95,20 +96,21 @@ jobs:
id: read-git-ref
run: |
echo "ref=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
- name: Update dev branch
- name: Update dev / next branch
if: env.SKIP_RELEASE == 'false'
continue-on-error: true # Prevent pipeline from failing when merge fails
env:
GITHUB_TOKEN: ${{ steps.obtainToken.outputs.token }}
UPSTREAM_BRANCH: ${{ github.ref_name == 'main' && 'dev' || 'next' }}
run: |
git config user.name "Releases Homarr"
git config user.email "175486441+homarr-releases[bot]@users.noreply.github.com"
git remote set-url origin https://x-access-token:${{ steps.obtainToken.outputs.token }}@github.com/${{ github.repository }}.git
git fetch origin dev
git checkout dev
git pull origin dev
git fetch origin ${{ env.UPSTREAM_BRANCH }}
git checkout ${{ env.UPSTREAM_BRANCH }}
git pull origin ${{ env.UPSTREAM_BRANCH }}
git rebase ${{ github.ref_name }}
git push origin dev
git push origin ${{ env.UPSTREAM_BRANCH }}
build-amd64:
name: Build docker image for amd64
needs: release
+24
View File
@@ -1,3 +1,27 @@
## [1.60.1-beta.2](https://github.com/homarr-labs/homarr/compare/v1.60.1-beta.1...v1.60.1-beta.2) (2026-05-07)
### Performance Improvements
* (next branch) reduce idle memory on startup ([#5637](https://github.com/homarr-labs/homarr/issues/5637)) ([5ee84f8](https://github.com/homarr-labs/homarr/commit/5ee84f8cf464c308729281cb4db572950ba85a82))
## [1.60.1-beta.1](https://github.com/homarr-labs/homarr/compare/v1.60.0...v1.60.1-beta.1) (2026-05-01)
### Performance Improvements
* **memory:** combine 3 node processes into one ([#5600](https://github.com/homarr-labs/homarr/issues/5600)) ([a1aa550](https://github.com/homarr-labs/homarr/commit/a1aa5505a365688b456090a882770e561479833e))
## [1.60.1-beta.1](https://github.com/homarr-labs/homarr/compare/v1.60.0...v1.60.1-beta.1) (2026-05-01)
### Performance Improvements
* **memory:** combine 3 node processes into one ([#5600](https://github.com/homarr-labs/homarr/issues/5600)) ([a1aa550](https://github.com/homarr-labs/homarr/commit/a1aa5505a365688b456090a882770e561479833e))
## [1.60.1-beta.1](https://github.com/homarr-labs/homarr/compare/v1.60.0...v1.60.1-beta.1) (2026-05-01)
### Performance Improvements
* **memory:** combine 3 node processes into one ([#5600](https://github.com/homarr-labs/homarr/issues/5600)) ([a1aa550](https://github.com/homarr-labs/homarr/commit/a1aa5505a365688b456090a882770e561479833e))
## [1.60.0](https://github.com/homarr-labs/homarr/compare/v1.59.3...v1.60.0) (2026-05-01)
### Features
-2
View File
@@ -43,8 +43,6 @@ RUN mkdir -p /var/cache/nginx && \
COPY --from=builder /app/apps/nextjs/next.config.ts .
COPY --from=builder /app/apps/nextjs/package.json .
COPY --from=builder /app/apps/tasks/tasks.cjs ./apps/tasks/tasks.cjs
COPY --from=builder /app/apps/websocket/wssServer.cjs ./apps/websocket/wssServer.cjs
COPY --from=builder /app/node_modules/better-sqlite3/build/Release/better_sqlite3.node /app/build/better_sqlite3.node
COPY --from=builder /app/packages/db/migrations ./db/migrations
+2
View File
@@ -32,6 +32,8 @@ const nextConfig: NextConfig = {
experimental: {
optimizePackageImports: ["@mantine/core", "@mantine/hooks", "@tabler/icons-react"],
turbopackFileSystemCacheForDev: true,
preloadEntriesOnStart: false,
webpackMemoryOptimizations: true,
},
transpilePackages: ["@homarr/ui", "@homarr/notifications", "@homarr/modals", "@homarr/spotlight", "@homarr/widgets"],
images: {
+2
View File
@@ -44,9 +44,11 @@
"@homarr/server-settings": "workspace:^0.1.0",
"@homarr/settings": "workspace:^0.1.0",
"@homarr/spotlight": "workspace:^0.1.0",
"@homarr/tasks": "workspace:^0.1.0",
"@homarr/translation": "workspace:^0.1.0",
"@homarr/ui": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"@homarr/websocket": "workspace:^0.1.0",
"@homarr/widgets": "workspace:^0.1.0",
"@mantine/colors-generator": "catalog:",
"@mantine/core": "catalog:",
+6
View File
@@ -0,0 +1,6 @@
export async function register() {
if (process.env.NEXT_RUNTIME === "nodejs") {
await import("@homarr/tasks");
await import("@homarr/websocket");
}
}
+1 -3
View File
@@ -5,12 +5,11 @@
"license": "Apache-2.0",
"type": "module",
"exports": {
".": "./src/index.ts"
".": "./src/main.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",
@@ -24,7 +23,6 @@
"@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",
-40
View File
@@ -1,52 +1,12 @@
// 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);
}
})();
+3 -1
View File
@@ -4,10 +4,12 @@
"private": true,
"license": "Apache-2.0",
"type": "module",
"exports": {
".": "./src/main.ts"
},
"main": "./src/main.ts",
"types": "./src/main.ts",
"scripts": {
"build": "esbuild src/main.ts --bundle --platform=node --outfile=wssServer.cjs --external:bcrypt --external:@opentelemetry/api --external:deasync --external:cpu-features --loader:.html=text --loader:.scss=text --loader:.node=text",
"clean": "rm -rf .turbo node_modules",
"dev": "pnpm with-env tsx ./src/main.ts",
"format": "prettier --check . --ignore-path ../../.gitignore",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "homarr",
"version": "1.60.0",
"version": "1.60.1-beta.2",
"private": true,
"scripts": {
"build": "cross-env CI=true turbo build",
@@ -15,7 +15,7 @@
"db:migration:sqlite:run": "pnpm -F db migration:sqlite:run",
"db:push": "pnpm -F db push:sqlite",
"db:studio": "pnpm -F db studio",
"dev": "turbo dev --parallel",
"dev": "pnpm -F nextjs dev",
"docker:dev": "docker compose -f ./development/development.docker-compose.yml up",
"format": "turbo format --continue -- --cache --cache-location node_modules/.cache/.prettiercache",
"format:fix": "turbo format --continue -- --write --cache --cache-location node_modules/.cache/.prettiercache",
-1
View File
@@ -25,7 +25,6 @@
"@homarr/auth": "workspace:^0.1.0",
"@homarr/common": "workspace:^0.1.0",
"@homarr/core": "workspace:^0.1.0",
"@homarr/cron-job-api": "workspace:^0.1.0",
"@homarr/cron-job-status": "workspace:^0.1.0",
"@homarr/cron-jobs": "workspace:^0.1.0",
"@homarr/db": "workspace:^0.1.0",
+19 -16
View File
@@ -2,33 +2,36 @@ import { observable } from "@trpc/server/observable";
import z from "zod/v4";
import { createLogger } from "@homarr/core/infrastructure/logs";
import { jobGroupKeys, jobNameSchema } from "@homarr/cron-job-api";
import { cronJobApi } from "@homarr/cron-job-api/client";
import type { TaskStatus } from "@homarr/cron-job-status";
import { createCronJobStatusChannel } from "@homarr/cron-job-status";
import { jobGroup } from "@homarr/cron-jobs";
import { JobManager } from "@homarr/cron-jobs/job-manager";
import { createTRPCRouter, permissionRequiredProcedure } from "../trpc";
const jobGroupKeys = jobGroup.getKeys();
const jobNameSchema = z.enum(jobGroup.getKeys());
const logger = createLogger({ module: "cronJobsRouter" });
export const cronJobsRouter = createTRPCRouter({
triggerJob: permissionRequiredProcedure
.requiresPermission("admin")
.input(jobNameSchema)
.mutation(async ({ input }) => {
await cronJobApi.trigger.mutate(input);
.mutation(async ({ input, ctx }) => {
await new JobManager(ctx.db, jobGroup).triggerAsync(input);
}),
startJob: permissionRequiredProcedure
.requiresPermission("admin")
.input(jobNameSchema)
.mutation(async ({ input }) => {
await cronJobApi.start.mutate(input);
.mutation(async ({ input, ctx }) => {
await new JobManager(ctx.db, jobGroup).startAsync(input);
}),
stopJob: permissionRequiredProcedure
.requiresPermission("admin")
.input(jobNameSchema)
.mutation(async ({ input }) => {
await cronJobApi.stop.mutate(input);
.mutation(({ input, ctx }) => {
new JobManager(ctx.db, jobGroup).stop(input);
}),
updateJobInterval: permissionRequiredProcedure
.requiresPermission("admin")
@@ -38,23 +41,23 @@ export const cronJobsRouter = createTRPCRouter({
cron: z.string(),
}),
)
.mutation(async ({ input }) => {
await cronJobApi.updateInterval.mutate(input);
.mutation(async ({ input, ctx }) => {
await new JobManager(ctx.db, jobGroup).updateIntervalAsync(input.name, input.cron);
}),
disableJob: permissionRequiredProcedure
.requiresPermission("admin")
.input(jobNameSchema)
.mutation(async ({ input }) => {
await cronJobApi.disable.mutate(input);
.mutation(async ({ input, ctx }) => {
await new JobManager(ctx.db, jobGroup).disableAsync(input);
}),
enableJob: permissionRequiredProcedure
.requiresPermission("admin")
.input(jobNameSchema)
.mutation(async ({ input }) => {
await cronJobApi.enable.mutate(input);
.mutation(async ({ input, ctx }) => {
await new JobManager(ctx.db, jobGroup).enableAsync(input);
}),
getJobs: permissionRequiredProcedure.requiresPermission("admin").query(async () => {
return await cronJobApi.getAll.query();
getJobs: permissionRequiredProcedure.requiresPermission("admin").query(async ({ ctx }) => {
return await new JobManager(ctx.db, jobGroup).getAllAsync();
}),
subscribeToStatusUpdates: permissionRequiredProcedure.requiresPermission("admin").subscription(() => {
return observable<TaskStatus>((emit) => {
-9
View File
@@ -1,9 +0,0 @@
import baseConfig from "@homarr/eslint-config/base";
/** @type {import('typescript-eslint').Config} */
export default [
{
ignores: [],
},
...baseConfig,
];
-46
View File
@@ -1,46 +0,0 @@
{
"name": "@homarr/cron-job-api",
"version": "0.1.0",
"private": true,
"license": "Apache-2.0",
"type": "module",
"exports": {
".": "./src/index.ts",
"./env": "./src/env.ts",
"./constants": "./src/constants.ts",
"./client": "./src/client.ts"
},
"typesVersions": {
"*": {
"*": [
"src/*"
]
}
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore",
"lint": "eslint",
"typecheck": "tsc --noEmit"
},
"prettier": "@homarr/prettier-config",
"dependencies": {
"@homarr/common": "workspace:^0.1.0",
"@homarr/core": "workspace:^0.1.0",
"@homarr/cron-jobs": "workspace:^0.1.0",
"@tanstack/react-query": "catalog:",
"@trpc/client": "catalog:",
"@trpc/server": "catalog:",
"@trpc/tanstack-react-query": "catalog:",
"react": "catalog:",
"zod": "catalog:"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/react": "catalog:",
"eslint": "catalog:",
"typescript": "catalog:"
}
}
-20
View File
@@ -1,20 +0,0 @@
import { createTRPCClient, httpLink } from "@trpc/client";
import type { JobRouter } from ".";
import { CRON_JOB_API_KEY_HEADER, CRON_JOB_API_PATH, CRON_JOB_API_PORT } from "./constants";
import { env } from "./env";
export const cronJobApi = createTRPCClient<JobRouter>({
links: [
httpLink({
url: `${getBaseUrl()}${CRON_JOB_API_PATH}`,
headers: {
[CRON_JOB_API_KEY_HEADER]: env.CRON_JOB_API_KEY,
},
}),
],
});
function getBaseUrl() {
return `http://localhost:${CRON_JOB_API_PORT}`;
}
-3
View File
@@ -1,3 +0,0 @@
export const CRON_JOB_API_PORT = 3002;
export const CRON_JOB_API_PATH = "/trpc";
export const CRON_JOB_API_KEY_HEADER = "cron-job-api-key";
-11
View File
@@ -1,11 +0,0 @@
import { z } from "zod/v4";
import { env as commonEnv } from "@homarr/common/env";
import { createEnv } from "@homarr/core/infrastructure/env";
export const env = createEnv({
server: {
CRON_JOB_API_KEY: commonEnv.NODE_ENV === "development" ? z.string().default("test") : z.string(),
},
experimental__runtimeEnv: process.env,
});
-85
View File
@@ -1,85 +0,0 @@
import { initTRPC, TRPCError } from "@trpc/server";
import { z } from "zod/v4";
import type { JobGroupKeys } from "@homarr/cron-jobs";
import { jobGroup } from "@homarr/cron-jobs";
import { env } from "./env";
export const jobGroupKeys = jobGroup.getKeys();
export const jobNameSchema = z.enum(jobGroup.getKeys());
export interface IJobManager {
startAsync(name: JobGroupKeys): Promise<void>;
triggerAsync(name: JobGroupKeys): Promise<void>;
stop(name: JobGroupKeys): void;
updateIntervalAsync(name: JobGroupKeys, cron: string): Promise<void>;
disableAsync(name: JobGroupKeys): Promise<void>;
enableAsync(name: JobGroupKeys): Promise<void>;
getAllAsync(): Promise<
{
name: JobGroupKeys;
cron: string;
preventManualExecution: boolean;
preventCustomInterval: boolean;
isEnabled: boolean;
}[]
>;
}
const t = initTRPC
.context<{
manager: IJobManager;
apiKey?: string;
}>()
.create();
const createTrpcRouter = t.router;
const apiKeyProcedure = t.procedure.use(({ ctx, next }) => {
if (ctx.apiKey !== env.CRON_JOB_API_KEY) {
throw new TRPCError({
code: "FORBIDDEN",
message: "Missing or invalid API key",
});
}
return next({
ctx: {
...ctx,
apiKey: undefined, // Clear the API key after checking
},
});
});
export const jobRouter = createTrpcRouter({
start: apiKeyProcedure.input(jobNameSchema).mutation(async ({ input, ctx }) => {
await ctx.manager.startAsync(input);
}),
trigger: apiKeyProcedure.input(jobNameSchema).mutation(async ({ input, ctx }) => {
await ctx.manager.triggerAsync(input);
}),
stop: apiKeyProcedure.input(jobNameSchema).mutation(({ input, ctx }) => {
ctx.manager.stop(input);
}),
updateInterval: apiKeyProcedure
.input(
z.object({
name: jobNameSchema,
cron: z.string(),
}),
)
.mutation(async ({ input, ctx }) => {
await ctx.manager.updateIntervalAsync(input.name, input.cron);
}),
disable: apiKeyProcedure.input(jobNameSchema).mutation(async ({ input, ctx }) => {
await ctx.manager.disableAsync(input);
}),
enable: apiKeyProcedure.input(jobNameSchema).mutation(async ({ input, ctx }) => {
await ctx.manager.enableAsync(input);
}),
getAll: apiKeyProcedure.query(({ ctx }) => {
return ctx.manager.getAllAsync();
}),
});
export type JobRouter = typeof jobRouter;
-8
View File
@@ -1,8 +0,0 @@
{
"extends": "@homarr/tsconfig/base.json",
"compilerOptions": {
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
},
"include": ["*.ts", "src"],
"exclude": ["node_modules"]
}
+4 -2
View File
@@ -5,7 +5,8 @@
"license": "Apache-2.0",
"type": "module",
"exports": {
".": "./index.ts"
".": "./index.ts",
"./job-manager": "./src/job-manager.ts"
},
"typesVersions": {
"*": {
@@ -38,7 +39,8 @@
"@homarr/server-settings": "workspace:^0.1.0",
"@homarr/translation": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"semver-parser": "catalog:"
"semver-parser": "catalog:",
"zod": "catalog:"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
+39 -28
View File
@@ -27,33 +27,44 @@ import { updateCheckerJob } from "./jobs/update-checker";
import { weatherJob } from "./jobs/weather";
import { createCronJobGroup } from "./lib";
export const jobGroup = createCronJobGroup({
analytics: analyticsJob,
iconsUpdater: iconsUpdaterJob,
ping: pingJob,
smartHomeEntityState: smartHomeEntityStateJob,
mediaServer: mediaServerJob,
mediaOrganizer: mediaOrganizerJob,
downloads: downloadsJob,
dnsHole: dnsHoleJob,
mediaRequestStats: mediaRequestStatsJob,
mediaRequestList: mediaRequestListJob,
rssFeeds: rssFeedsJob,
indexerManager: indexerManagerJob,
healthMonitoring: healthMonitoringJob,
updateChecker: updateCheckerJob,
mediaTranscoding: mediaTranscodingJob,
minecraftServerStatus: minecraftServerStatusJob,
dockerContainers: dockerContainersJob,
networkController: networkControllerJob,
firewallCpu: firewallCpuJob,
firewallMemory: firewallMemoryJob,
firewallVersion: firewallVersionJob,
firewallInterfaces: firewallInterfacesJob,
refreshNotifications: refreshNotificationsJob,
weather: weatherJob,
timetable: timetableJob,
tracearr: tracearrJob,
});
const getJobGroup = () => {
return createCronJobGroup({
analytics: analyticsJob,
iconsUpdater: iconsUpdaterJob,
ping: pingJob,
smartHomeEntityState: smartHomeEntityStateJob,
mediaServer: mediaServerJob,
mediaOrganizer: mediaOrganizerJob,
downloads: downloadsJob,
dnsHole: dnsHoleJob,
mediaRequestStats: mediaRequestStatsJob,
mediaRequestList: mediaRequestListJob,
rssFeeds: rssFeedsJob,
indexerManager: indexerManagerJob,
healthMonitoring: healthMonitoringJob,
updateChecker: updateCheckerJob,
mediaTranscoding: mediaTranscodingJob,
minecraftServerStatus: minecraftServerStatusJob,
dockerContainers: dockerContainersJob,
networkController: networkControllerJob,
firewallCpu: firewallCpuJob,
firewallMemory: firewallMemoryJob,
firewallVersion: firewallVersionJob,
firewallInterfaces: firewallInterfacesJob,
refreshNotifications: refreshNotificationsJob,
weather: weatherJob,
timetable: timetableJob,
tracearr: tracearrJob,
});
};
// Add global type augmentation for homarr
declare global {
var cronJobs: ReturnType<typeof getJobGroup> | undefined;
}
global.cronJobs ??= getJobGroup();
export const jobGroup = global.cronJobs;
export type JobGroupKeys = ReturnType<(typeof jobGroup)["getKeys"]>[number];
@@ -1,7 +1,6 @@
import { Cron } from "croner";
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";
@@ -9,7 +8,7 @@ import { cronJobConfigurations } from "@homarr/db/schema";
const logger = createLogger({ module: "jobManager" });
export class JobManager implements IJobManager {
export class JobManager {
constructor(
private db: Database,
private jobGroup: typeof cronJobGroup,
+9 -55
View File
@@ -751,6 +751,9 @@ importers:
'@homarr/spotlight':
specifier: workspace:^0.1.0
version: link:../../packages/spotlight
'@homarr/tasks':
specifier: workspace:^0.1.0
version: link:../tasks
'@homarr/translation':
specifier: workspace:^0.1.0
version: link:../../packages/translation
@@ -760,6 +763,9 @@ importers:
'@homarr/validation':
specifier: workspace:^0.1.0
version: link:../../packages/validation
'@homarr/websocket':
specifier: workspace:^0.1.0
version: link:../websocket
'@homarr/widgets':
specifier: workspace:^0.1.0
version: link:../../packages/widgets
@@ -944,9 +950,6 @@ importers:
'@homarr/core':
specifier: workspace:^
version: link:../../packages/core
'@homarr/cron-job-api':
specifier: workspace:^0.1.0
version: link:../../packages/cron-job-api
'@homarr/cron-jobs':
specifier: workspace:^0.1.0
version: link:../../packages/cron-jobs
@@ -1148,9 +1151,6 @@ importers:
'@homarr/core':
specifier: workspace:^0.1.0
version: link:../core
'@homarr/cron-job-api':
specifier: workspace:^0.1.0
version: link:../cron-job-api
'@homarr/cron-job-status':
specifier: workspace:^0.1.0
version: link:../cron-job-status
@@ -1503,55 +1503,6 @@ importers:
specifier: 'catalog:'
version: 5.9.3
packages/cron-job-api:
dependencies:
'@homarr/common':
specifier: workspace:^0.1.0
version: link:../common
'@homarr/core':
specifier: workspace:^0.1.0
version: link:../core
'@homarr/cron-jobs':
specifier: workspace:^0.1.0
version: link:../cron-jobs
'@tanstack/react-query':
specifier: 'catalog:'
version: 5.100.5(react@19.2.5)
'@trpc/client':
specifier: 'catalog:'
version: 11.16.0(@trpc/server@11.16.0(typescript@5.9.3))(typescript@5.9.3)
'@trpc/server':
specifier: 'catalog:'
version: 11.16.0(typescript@5.9.3)
'@trpc/tanstack-react-query':
specifier: 'catalog:'
version: 11.16.0(@tanstack/react-query@5.100.5(react@19.2.5))(@trpc/client@11.16.0(@trpc/server@11.16.0(typescript@5.9.3))(typescript@5.9.3))(@trpc/server@11.16.0(typescript@5.9.3))(react@19.2.5)(typescript@5.9.3)
react:
specifier: 'catalog:'
version: 19.2.5
zod:
specifier: 'catalog:'
version: 4.3.6
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
'@types/react':
specifier: 'catalog:'
version: 19.2.14
eslint:
specifier: 'catalog:'
version: 9.39.4
typescript:
specifier: 'catalog:'
version: 5.9.3
packages/cron-job-status:
dependencies:
'@homarr/redis':
@@ -1627,6 +1578,9 @@ importers:
semver-parser:
specifier: 'catalog:'
version: 4.1.8
zod:
specifier: 'catalog:'
version: 4.3.6
devDependencies:
'@homarr/eslint-config':
specifier: workspace:^0.2.0
+24 -11
View File
@@ -16,8 +16,6 @@ fi
# Auth secret is generated every time the container starts as it is required, but not used because we don't need JWTs or Mail hashing
export AUTH_SECRET=$(openssl rand -base64 32)
# Cron job API key is generated every time the container starts as it is required for communication between nextjs-api and tasks-api
export CRON_JOB_API_KEY=$(openssl rand -base64 32)
# Support secret file for encryption key (is not empty and file exists)
if [ -n "$SECRET_ENCRYPTION_KEY_FILE" ] && [ -f "$SECRET_ENCRYPTION_KEY_FILE" ]; then
@@ -43,21 +41,36 @@ else
REDIS_PID=$!
fi
node apps/tasks/tasks.cjs &
TASKS_PID=$!
node apps/websocket/wssServer.cjs &
WSS_PID=$!
node apps/nextjs/server.js &
NEXTJS_PID=$!
report_ram() {
INTERVAL=${RAM_REPORT_INTERVAL:-1800}
while true; do
sleep "$INTERVAL"
MEM_USED_BYTES=$(cat /sys/fs/cgroup/memory.current)
MEM_MAX_RAW=$(cat /sys/fs/cgroup/memory.max)
MEM_USED_MB=$((MEM_USED_BYTES / 1048576))
if [ "$MEM_MAX_RAW" = "max" ]; then
MEM_MAX_MB=0
else
MEM_MAX_MB=$((MEM_MAX_RAW / 1048576))
fi
wget -q --post-data="{\"api_key\":\"phc_pWxeD1hbl4ip02JYReX1Crjkt5DhB3dduigirHMCtFE\",\"event\":\"ram_usage\",\"distinct_id\":\"${HOSTNAME:-homarr}\",\"properties\":{\"ram_used_mb\":${MEM_USED_MB},\"ram_max_mb\":${MEM_MAX_MB},\"ram_used_bytes\":${MEM_USED_BYTES},\"\$process_person_profile\":false}}" \
--header="Content-Type: application/json" \
-O /dev/null https://hog.ajnart.dev/i/v0/e/ 2>/dev/null
echo "Reported RAM: ${MEM_USED_MB}MB / ${MEM_MAX_MB}MB"
done
}
report_ram &
REPORT_RAM_PID=$!
# Function to handle SIGTERM and shut down services
terminate() {
echo "Received SIGTERM. Shutting down..."
kill -TERM $NGINX_PID $TASKS_PID $WSS_PID $NEXTJS_PID 2>/dev/null
kill -TERM $REPORT_RAM_PID $NGINX_PID $NEXTJS_PID 2>/dev/null
wait
# kill redis-server last because of logging of other services and only if $REDIS_PID is set
if [ -n "$REDIS_PID" ]; then