Compare commits

..
Author SHA1 Message Date
Meier Lukas da320c113d fix: unused import 2026-05-10 18:08:38 +02:00
Meier Lukas 559a666589 fix(docker): improve icon matching 2026-05-10 18:03:19 +02:00
44 changed files with 6659 additions and 5400 deletions

No files matched your search

-4
View File
@@ -10,7 +10,3 @@ README.md
dev dev
.build .build
e2e e2e
apps/nextjs/.next
.env
.env.*
!.env.example
+4 -8
View File
@@ -12,6 +12,10 @@ AUTH_SECRET="supersecret"
# or starting the project without any (which will show a randomly generated one). # or starting the project without any (which will show a randomly generated one).
SECRET_ENCRYPTION_KEY=0000000000000000000000000000000000000000000000000000000000000000 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' LOG_LEVEL='info'
# This is how you can use the sqlite driver: # This is how you can use the sqlite driver:
@@ -41,14 +45,6 @@ DB_URL='FULL_PATH_TO_YOUR_SQLITE_DB_FILE'
TURBO_TELEMETRY_DISABLED=1 TURBO_TELEMETRY_DISABLED=1
# Turborepo remote cache (optional, for faster CI/local builds)
# Uncomment and fill in to enable remote caching locally.
# CI uses GitHub secrets for these values automatically.
# TURBO_API=https://your-cache-worker.workers.dev
# TURBO_TEAM=team_your_team
# TURBO_TOKEN=your-turbo-token
# TURBO_REMOTE_CACHE_SIGNATURE_KEY=your-signature-key
# Comma-separated paths to Docker-compatible Unix sockets. # Comma-separated paths to Docker-compatible Unix sockets.
# Useful for connecting to multiple runtimes (Docker + Podman) simultaneously. # Useful for connecting to multiple runtimes (Docker + Podman) simultaneously.
# When unset and no TCP hosts configured, defaults to /var/run/docker.sock. # When unset and no TCP hosts configured, defaults to /var/run/docker.sock.
+5 -24
View File
@@ -15,13 +15,6 @@ concurrency:
env: env:
FORCE_COLOR: 3 FORCE_COLOR: 3
SKIP_ENV_VALIDATION: true
TURBO_API: ${{ secrets.TURBO_API }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_REMOTE_CACHE_SIGNATURE_KEY: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }}
TURBO_SCM_BASE: ${{ github.event.pull_request.base.sha || github.event.before || 'HEAD~1' }}
TURBO_SCM_HEAD: ${{ github.sha }}
jobs: jobs:
lint: lint:
@@ -29,8 +22,6 @@ jobs:
timeout-minutes: 10 timeout-minutes: 10
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Setup - name: Setup
uses: ./tooling/github/setup uses: ./tooling/github/setup
@@ -40,35 +31,31 @@ jobs:
run: cp .env.example .env run: cp .env.example .env
- name: Lint - name: Lint
run: pnpm turbo lint --affected && pnpm lint:ws run: pnpm lint && pnpm lint:ws
format: format:
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 10 timeout-minutes: 10
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Setup - name: Setup
uses: ./tooling/github/setup uses: ./tooling/github/setup
- name: Format - name: Format
run: pnpm turbo format --affected --continue -- --cache --cache-location node_modules/.cache/.prettiercache run: pnpm format
typecheck: typecheck:
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 10 timeout-minutes: 10
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Setup - name: Setup
uses: ./tooling/github/setup uses: ./tooling/github/setup
- name: Typecheck - name: Typecheck
run: pnpm turbo typecheck --affected run: pnpm typecheck
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -91,12 +78,11 @@ jobs:
e2e: e2e:
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 15 timeout-minutes: 15
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v6
- name: Setup - name: Setup
uses: ./tooling/github/setup uses: ./tooling/github/setup
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Build docker image - name: Build docker image
id: build-docker-image id: build-docker-image
uses: docker/build-push-action@v7 uses: docker/build-push-action@v7
@@ -104,11 +90,8 @@ jobs:
platforms: linux/amd64 platforms: linux/amd64
context: . context: .
push: false push: false
load: true
tags: homarr-e2e tags: homarr-e2e
network: host network: host
cache-from: type=gha
cache-to: type=gha,mode=max
env: env:
SKIP_ENV_VALIDATION: true SKIP_ENV_VALIDATION: true
- name: Install playwright browsers - name: Install playwright browsers
@@ -122,12 +105,10 @@ jobs:
timeout-minutes: 10 timeout-minutes: 10
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Setup - name: Setup
uses: ./tooling/github/setup uses: ./tooling/github/setup
- name: Copy env - name: Copy env
shell: bash shell: bash
run: cp .env.example .env run: cp .env.example .env
- name: Build - name: Build
run: pnpm turbo build --affected run: pnpm build
+8 -10
View File
@@ -5,7 +5,6 @@ on:
branches: branches:
- main - main
- beta - beta
- next
- dev - dev
workflow_dispatch: workflow_dispatch:
inputs: inputs:
@@ -35,7 +34,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 5 timeout-minutes: 5
env: env:
SKIP_RELEASE: ${{ github.event_name == 'workflow_dispatch' || github.ref_name == 'dev' || github.ref_name == 'next' }} SKIP_RELEASE: ${{ github.event_name == 'workflow_dispatch' || github.ref_name == 'dev' }}
outputs: outputs:
version: ${{ steps.read-semver.outputs.version || steps.version-fallback.outputs.version }} version: ${{ steps.read-semver.outputs.version || steps.version-fallback.outputs.version }}
git_ref: ${{ steps.read-git-ref.outputs.ref || github.ref }} git_ref: ${{ steps.read-git-ref.outputs.ref || github.ref }}
@@ -55,7 +54,7 @@ jobs:
with: with:
private-key: ${{ secrets.RENOVATE_MERGE_PRIVATE_KEY }} private-key: ${{ secrets.RENOVATE_MERGE_PRIVATE_KEY }}
client-id: ${{ secrets.RENOVATE_MERGE_APP_ID }} client-id: ${{ secrets.RENOVATE_MERGE_APP_ID }}
permission-contents: write # required to commit package.json & changelog changes, merge them to dev / next and publish the release permission-contents: write # required to commit package.json & changelog changes, merge them to dev and publish the release
- uses: actions/checkout@v6 - uses: actions/checkout@v6
if: env.SKIP_RELEASE == 'false' if: env.SKIP_RELEASE == 'false'
@@ -67,7 +66,7 @@ jobs:
if: env.SKIP_RELEASE == 'false' if: env.SKIP_RELEASE == 'false'
with: with:
node-version: 24.15.0 node-version: 24.15.0
package-manager-cache: false cache: "pnpm"
- run: npm i -g pnpm - run: npm i -g pnpm
if: env.SKIP_RELEASE == 'false' if: env.SKIP_RELEASE == 'false'
- name: Install dependencies - name: Install dependencies
@@ -96,21 +95,20 @@ jobs:
id: read-git-ref id: read-git-ref
run: | run: |
echo "ref=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" echo "ref=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
- name: Update dev / next branch - name: Update dev branch
if: env.SKIP_RELEASE == 'false' if: env.SKIP_RELEASE == 'false'
continue-on-error: true # Prevent pipeline from failing when merge fails continue-on-error: true # Prevent pipeline from failing when merge fails
env: env:
GITHUB_TOKEN: ${{ steps.obtainToken.outputs.token }} GITHUB_TOKEN: ${{ steps.obtainToken.outputs.token }}
UPSTREAM_BRANCH: ${{ github.ref_name == 'main' && 'dev' || 'next' }}
run: | run: |
git config user.name "Releases Homarr" git config user.name "Releases Homarr"
git config user.email "175486441+homarr-releases[bot]@users.noreply.github.com" 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 remote set-url origin https://x-access-token:${{ steps.obtainToken.outputs.token }}@github.com/${{ github.repository }}.git
git fetch origin ${{ env.UPSTREAM_BRANCH }} git fetch origin dev
git checkout ${{ env.UPSTREAM_BRANCH }} git checkout dev
git pull origin ${{ env.UPSTREAM_BRANCH }} git pull origin dev
git rebase ${{ github.ref_name }} git rebase ${{ github.ref_name }}
git push origin ${{ env.UPSTREAM_BRANCH }} git push origin dev
build-amd64: build-amd64:
name: Build docker image for amd64 name: Build docker image for amd64
needs: release needs: release
+3838 -3857
View File
File diff suppressed because it is too large. Load diff
+2
View File
@@ -43,6 +43,8 @@ RUN mkdir -p /var/cache/nginx && \
COPY --from=builder /app/apps/nextjs/next.config.ts . COPY --from=builder /app/apps/nextjs/next.config.ts .
COPY --from=builder /app/apps/nextjs/package.json . 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/node_modules/better-sqlite3/build/Release/better_sqlite3.node /app/build/better_sqlite3.node
COPY --from=builder /app/packages/db/migrations ./db/migrations COPY --from=builder /app/packages/db/migrations ./db/migrations
+5 -2
View File
@@ -6,6 +6,7 @@ import "@homarr/core/infrastructure/logs/env";
import "@homarr/docker/env"; import "@homarr/docker/env";
import type { NextConfig } from "next"; import type { NextConfig } from "next";
import MillionLint from "@million/lint";
import createNextIntlPlugin from "next-intl/plugin"; import createNextIntlPlugin from "next-intl/plugin";
// Package path does not work... so we need to use relative path // Package path does not work... so we need to use relative path
@@ -31,8 +32,6 @@ const nextConfig: NextConfig = {
experimental: { experimental: {
optimizePackageImports: ["@mantine/core", "@mantine/hooks", "@tabler/icons-react"], optimizePackageImports: ["@mantine/core", "@mantine/hooks", "@tabler/icons-react"],
turbopackFileSystemCacheForDev: true, turbopackFileSystemCacheForDev: true,
preloadEntriesOnStart: false,
webpackMemoryOptimizations: true,
}, },
transpilePackages: ["@homarr/ui", "@homarr/notifications", "@homarr/modals", "@homarr/spotlight", "@homarr/widgets"], transpilePackages: ["@homarr/ui", "@homarr/notifications", "@homarr/modals", "@homarr/spotlight", "@homarr/widgets"],
images: { images: {
@@ -84,4 +83,8 @@ const nextConfig: NextConfig = {
}, },
}; };
// Skip transform is used because of webpack loader, without it for example 'Tooltip.Floating' will not work and show an error
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const withMillionLint = MillionLint.next({ rsc: true, skipTransform: true, telemetry: false });
export default withNextIntl(nextConfig); export default withNextIntl(nextConfig);
+1 -2
View File
@@ -44,11 +44,9 @@
"@homarr/server-settings": "workspace:^0.1.0", "@homarr/server-settings": "workspace:^0.1.0",
"@homarr/settings": "workspace:^0.1.0", "@homarr/settings": "workspace:^0.1.0",
"@homarr/spotlight": "workspace:^0.1.0", "@homarr/spotlight": "workspace:^0.1.0",
"@homarr/tasks": "workspace:^0.1.0",
"@homarr/translation": "workspace:^0.1.0", "@homarr/translation": "workspace:^0.1.0",
"@homarr/ui": "workspace:^0.1.0", "@homarr/ui": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0",
"@homarr/websocket": "workspace:^0.1.0",
"@homarr/widgets": "workspace:^0.1.0", "@homarr/widgets": "workspace:^0.1.0",
"@mantine/colors-generator": "catalog:", "@mantine/colors-generator": "catalog:",
"@mantine/core": "catalog:", "@mantine/core": "catalog:",
@@ -56,6 +54,7 @@
"@mantine/hooks": "catalog:", "@mantine/hooks": "catalog:",
"@mantine/modals": "catalog:", "@mantine/modals": "catalog:",
"@mantine/tiptap": "catalog:", "@mantine/tiptap": "catalog:",
"@million/lint": "catalog:",
"@tabler/icons-react": "catalog:", "@tabler/icons-react": "catalog:",
"@tanstack/react-query": "catalog:", "@tanstack/react-query": "catalog:",
"@tanstack/react-query-devtools": "catalog:", "@tanstack/react-query-devtools": "catalog:",
@@ -1,27 +1,70 @@
"use client"; "use client";
import { Box, Stack } from "@mantine/core"; import { useCallback, useRef } from "react";
import { Box, LoadingOverlay, Stack } from "@mantine/core";
import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
import { useCurrentLayout, useRequiredBoard } from "@homarr/boards/context"; import { useCurrentLayout, useRequiredBoard } from "@homarr/boards/context";
import { BoardCategorySection } from "~/components/board/sections/category-section"; import { BoardCategorySection } from "~/components/board/sections/category-section";
import { BoardEmptySection } from "~/components/board/sections/empty-section"; import { BoardEmptySection } from "~/components/board/sections/empty-section";
import { BoardBackgroundVideo } from "~/components/layout/background"; import { BoardBackgroundVideo } from "~/components/layout/background";
import { fullHeightWithoutHeaderAndFooter } from "~/constants";
import { useIsBoardReady } from "./_ready-context";
let boardName: string | null = null;
export const updateBoardName = (name: string | null) => {
boardName = name;
};
type UpdateCallback = (prev: RouterOutputs["board"]["getHomeBoard"]) => RouterOutputs["board"]["getHomeBoard"];
export const useUpdateBoard = () => {
const utils = clientApi.useUtils();
const updateBoard = useCallback(
(updaterWithoutUndefined: UpdateCallback) => {
if (!boardName) {
throw new Error("Board name is not set");
}
utils.board.getBoardByName.setData({ name: boardName }, (previous) =>
previous ? updaterWithoutUndefined(previous) : previous,
);
},
[utils],
);
return {
updateBoard,
};
};
export const ClientBoard = () => { export const ClientBoard = () => {
const board = useRequiredBoard(); const board = useRequiredBoard();
const currentLayoutId = useCurrentLayout(); const currentLayoutId = useCurrentLayout();
const isReady = useIsBoardReady();
const fullWidthSortedSections = board.sections const fullWidthSortedSections = board.sections
.filter((section) => section.kind === "empty" || section.kind === "category") .filter((section) => section.kind === "empty" || section.kind === "category")
.sort((sectionA, sectionB) => sectionA.yOffset - sectionB.yOffset); .sort((sectionA, sectionB) => sectionA.yOffset - sectionB.yOffset);
const ref = useRef<HTMLDivElement>(null);
return ( return (
<Box h="100%" pos="relative"> <Box h="100%" pos="relative">
<BoardBackgroundVideo /> <BoardBackgroundVideo />
<Stack h="100%"> <LoadingOverlay
visible={!isReady}
transitionProps={{ duration: 500 }}
loaderProps={{ size: "lg" }}
h={fullHeightWithoutHeaderAndFooter}
/>
<Stack ref={ref} h="100%" style={{ visibility: isReady ? "visible" : "hidden" }}>
{fullWidthSortedSections.map((section) => {fullWidthSortedSections.map((section) =>
section.kind === "empty" ? ( section.kind === "empty" ? (
// Unique keys per layout to always reinitialize the gridstack
<BoardEmptySection key={`${currentLayoutId}-${section.id}`} section={section} /> <BoardEmptySection key={`${currentLayoutId}-${section.id}`} section={section} />
) : ( ) : (
<BoardCategorySection key={`${currentLayoutId}-${section.id}`} section={section} /> <BoardCategorySection key={`${currentLayoutId}-${section.id}`} section={section} />
@@ -1,19 +1,42 @@
"use client"; "use client";
import type { PropsWithChildren } from "react"; import type { PropsWithChildren } from "react";
import { createContext, useCallback, useContext } from "react"; import { createContext, useCallback, useContext, useEffect, useState } from "react";
import { usePathname } from "next/navigation";
import { clientApi } from "@homarr/api/client";
import { useRequiredBoard } from "@homarr/boards/context";
const BoardReadyContext = createContext<{ const BoardReadyContext = createContext<{
isReady: boolean;
markAsReady: (id: string) => void; markAsReady: (id: string) => void;
} | null>(null); } | null>(null);
export const BoardReadyProvider = ({ children }: PropsWithChildren) => { export const BoardReadyProvider = ({ children }: PropsWithChildren) => {
// eslint-disable-next-line @typescript-eslint/no-empty-function const pathname = usePathname();
const markAsReady = useCallback((_id: string) => {}, []); const utils = clientApi.useUtils();
const board = useRequiredBoard();
const [readySections, setReadySections] = useState<string[]>([]);
// Reset sections required for ready state
useEffect(() => {
return () => {
setReadySections([]);
};
}, [pathname, utils]);
useEffect(() => {
setReadySections((previous) => previous.filter((id) => board.sections.some((section) => section.id === id)));
}, [board.sections.length, setReadySections]);
const markAsReady = useCallback((id: string) => {
setReadySections((previous) => (previous.includes(id) ? previous : [...previous, id]));
}, []);
return ( return (
<BoardReadyContext.Provider <BoardReadyContext.Provider
value={{ value={{
isReady: board.sections.length === readySections.length,
markAsReady, markAsReady,
}} }}
> >
@@ -31,3 +54,13 @@ export const useMarkSectionAsReady = () => {
return context.markAsReady; return context.markAsReady;
}; };
export const useIsBoardReady = () => {
const context = useContext(BoardReadyContext);
if (!context) {
throw new Error("BoardReadyProvider is required");
}
return context.isReady;
};
-6
View File
@@ -1,6 +0,0 @@
export async function register() {
if (process.env.NEXT_RUNTIME === "nodejs") {
await import("@homarr/tasks");
await import("@homarr/websocket");
}
}
+3 -1
View File
@@ -5,11 +5,12 @@
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
"exports": { "exports": {
".": "./src/main.ts" ".": "./src/index.ts"
}, },
"main": "./src/main.ts", "main": "./src/main.ts",
"types": "./src/main.ts", "types": "./src/main.ts",
"scripts": { "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", "clean": "rm -rf .turbo node_modules",
"dev": "pnpm with-env tsx ./src/main.ts", "dev": "pnpm with-env tsx ./src/main.ts",
"format": "prettier --check . --ignore-path ../../.gitignore", "format": "prettier --check . --ignore-path ../../.gitignore",
@@ -23,6 +24,7 @@
"@homarr/auth": "workspace:^0.1.0", "@homarr/auth": "workspace:^0.1.0",
"@homarr/common": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0",
"@homarr/core": "workspace:^", "@homarr/core": "workspace:^",
"@homarr/cron-job-api": "workspace:^0.1.0",
"@homarr/cron-jobs": "workspace:^0.1.0", "@homarr/cron-jobs": "workspace:^0.1.0",
"@homarr/cron-jobs-core": "workspace:^0.1.0", "@homarr/cron-jobs-core": "workspace:^0.1.0",
"@homarr/db": "workspace:^0.1.0", "@homarr/db": "workspace:^0.1.0",
@@ -1,6 +1,7 @@
import { Cron } from "croner"; import { Cron } from "croner";
import { createLogger } from "@homarr/core/infrastructure/logs"; 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 { jobGroup as cronJobGroup, JobGroupKeys } from "@homarr/cron-jobs";
import type { Database, InferInsertModel } from "@homarr/db"; import type { Database, InferInsertModel } from "@homarr/db";
import { eq } from "@homarr/db"; import { eq } from "@homarr/db";
@@ -8,7 +9,7 @@ import { cronJobConfigurations } from "@homarr/db/schema";
const logger = createLogger({ module: "jobManager" }); const logger = createLogger({ module: "jobManager" });
export class JobManager { export class JobManager implements IJobManager {
constructor( constructor(
private db: Database, private db: Database,
private jobGroup: typeof cronJobGroup, private jobGroup: typeof cronJobGroup,
+41 -1
View File
@@ -1,12 +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. // 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 "./overrides";
import { jobGroup } from "@homarr/cron-jobs"; 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"; 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 () => { void (async () => {
await onStartAsync(); await onStartAsync();
await jobGroup.initializeAsync(); await jobGroup.initializeAsync();
await jobGroup.startAllAsync(); 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);
}
})(); })();
+1 -1
View File
@@ -9,5 +9,5 @@
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json" "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
}, },
"include": ["."], "include": ["."],
"exclude": ["node_modules", "*.cjs", "*.css"] "exclude": ["node_modules"]
} }
+1 -3
View File
@@ -4,12 +4,10 @@
"private": true, "private": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
"exports": {
".": "./src/main.ts"
},
"main": "./src/main.ts", "main": "./src/main.ts",
"types": "./src/main.ts", "types": "./src/main.ts",
"scripts": { "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", "clean": "rm -rf .turbo node_modules",
"dev": "pnpm with-env tsx ./src/main.ts", "dev": "pnpm with-env tsx ./src/main.ts",
"format": "prettier --check . --ignore-path ../../.gitignore", "format": "prettier --check . --ignore-path ../../.gitignore",
+1 -1
View File
@@ -9,5 +9,5 @@
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json" "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
}, },
"include": ["."], "include": ["."],
"exclude": ["node_modules", "*.cjs", "*.css"] "exclude": ["node_modules"]
} }
+3 -3
View File
@@ -2,12 +2,12 @@
"name": "homarr-prebuilt-debian", "name": "homarr-prebuilt-debian",
"private": true, "private": true,
"dependencies": { "dependencies": {
"better-sqlite3": "^12.10.0" "better-sqlite3": "^12.9.0"
}, },
"packageManager": "pnpm@10.33.4", "packageManager": "pnpm@10.33.3",
"engines": { "engines": {
"node": ">=24.15.0", "node": ">=24.15.0",
"pnpm": ">=10.33.4" "pnpm": ">=10.33.3"
}, },
"pnpm": { "pnpm": {
"onlyBuiltDependencies": [ "onlyBuiltDependencies": [
+6 -6
View File
@@ -9,17 +9,17 @@ importers:
.: .:
dependencies: dependencies:
better-sqlite3: better-sqlite3:
specifier: ^12.10.0 specifier: ^12.9.0
version: 12.10.0 version: 12.9.0
packages: packages:
base64-js@1.5.1: base64-js@1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
better-sqlite3@12.10.0: better-sqlite3@12.9.0:
resolution: {integrity: sha512-CyzaZRQKyHkB2ZInfTTl2nvT33EbDpjkLEbE8/Zck3Ll6O0qqvuGdrJ45HgtH+HykRg88ITY3AdreBGN70aBSQ==} resolution: {integrity: sha512-wqUv4Gm3toFpHDQmaKD4QhZm3g1DjUBI0yzS4UBl6lElUmXFYdTQmmEDpAFa5o8FiFiymURypEnfVHzILKaxqQ==}
engines: {node: 20.x || 22.x || 23.x || 24.x || 25.x || 26.x} engines: {node: 20.x || 22.x || 23.x || 24.x || 25.x}
bindings@1.5.0: bindings@1.5.0:
resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==}
@@ -148,7 +148,7 @@ snapshots:
base64-js@1.5.1: {} base64-js@1.5.1: {}
better-sqlite3@12.10.0: better-sqlite3@12.9.0:
dependencies: dependencies:
bindings: 1.5.0 bindings: 1.5.0
prebuild-install: 7.1.3 prebuild-install: 7.1.3
+1 -1
View File
@@ -1,4 +1,4 @@
import { chromium } from "@playwright/test"; import { chromium } from "playwright";
import { GenericContainer } from "testcontainers"; import { GenericContainer } from "testcontainers";
import { describe, expect, test } from "vitest"; import { describe, expect, test } from "vitest";
+1 -1
View File
@@ -1,4 +1,4 @@
import { chromium } from "@playwright/test"; import { chromium } from "playwright";
import { describe, test } from "vitest"; import { describe, test } from "vitest";
import { OnboardingActions } from "./shared/actions/onboarding-actions"; import { OnboardingActions } from "./shared/actions/onboarding-actions";
+1 -1
View File
@@ -1,5 +1,5 @@
import { createId } from "@paralleldrive/cuid2"; import { createId } from "@paralleldrive/cuid2";
import type { Page } from "@playwright/test"; import type { Page } from "playwright";
import * as sqliteSchema from "../../../packages/db/schema/sqlite"; import * as sqliteSchema from "../../../packages/db/schema/sqlite";
import type { SqliteDatabase } from "../e2e-db"; import type { SqliteDatabase } from "../e2e-db";
@@ -1,5 +1,5 @@
import type { Page } from "@playwright/test";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import type { Page } from "playwright";
import { expect } from "vitest"; import { expect } from "vitest";
import * as sqliteSchema from "../../../packages/db/schema/sqlite"; import * as sqliteSchema from "../../../packages/db/schema/sqlite";
+12 -14
View File
@@ -1,6 +1,6 @@
{ {
"name": "homarr", "name": "homarr",
"version": "1.61.1-beta.1", "version": "1.61.0",
"private": true, "private": true,
"scripts": { "scripts": {
"build": "cross-env CI=true turbo build", "build": "cross-env CI=true turbo build",
@@ -15,7 +15,7 @@
"db:migration:sqlite:run": "pnpm -F db migration:sqlite:run", "db:migration:sqlite:run": "pnpm -F db migration:sqlite:run",
"db:push": "pnpm -F db push:sqlite", "db:push": "pnpm -F db push:sqlite",
"db:studio": "pnpm -F db studio", "db:studio": "pnpm -F db studio",
"dev": "pnpm -F nextjs dev", "dev": "turbo dev --parallel",
"docker:dev": "docker compose -f ./development/development.docker-compose.yml up", "docker:dev": "docker compose -f ./development/development.docker-compose.yml up",
"format": "turbo format --continue -- --cache --cache-location node_modules/.cache/.prettiercache", "format": "turbo format --continue -- --cache --cache-location node_modules/.cache/.prettiercache",
"format:fix": "turbo format --continue -- --write --cache --cache-location node_modules/.cache/.prettiercache", "format:fix": "turbo format --continue -- --write --cache --cache-location node_modules/.cache/.prettiercache",
@@ -36,7 +36,6 @@
"prettier": "@homarr/prettier-config", "prettier": "@homarr/prettier-config",
"devDependencies": { "devDependencies": {
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@playwright/test": "catalog:",
"@semantic-release/changelog": "catalog:", "@semantic-release/changelog": "catalog:",
"@semantic-release/commit-analyzer": "catalog:", "@semantic-release/commit-analyzer": "catalog:",
"@semantic-release/git": "catalog:", "@semantic-release/git": "catalog:",
@@ -60,10 +59,10 @@
"vite-tsconfig-paths": "catalog:", "vite-tsconfig-paths": "catalog:",
"vitest": "catalog:" "vitest": "catalog:"
}, },
"packageManager": "pnpm@10.33.4", "packageManager": "pnpm@10.33.3",
"engines": { "engines": {
"node": ">=24.15.0", "node": ">=24.15.0",
"pnpm": ">=10.33.4" "pnpm": ">=10.33.3"
}, },
"pnpm": { "pnpm": {
"onlyBuiltDependencies": [ "onlyBuiltDependencies": [
@@ -80,23 +79,23 @@
"overrides": { "overrides": {
"@babel/helpers@<7.26.10": ">=7.29.2", "@babel/helpers@<7.26.10": ">=7.29.2",
"@babel/runtime@<7.26.10": ">=7.29.2", "@babel/runtime@<7.26.10": ">=7.29.2",
"axios@>=1.0.0 <1.8.2": ">=1.16.1", "axios@>=1.0.0 <1.8.2": ">=1.16.0",
"brace-expansion@>=2.0.0 <=2.0.1": ">=5.0.6", "brace-expansion@>=2.0.0 <=2.0.1": ">=5.0.5",
"brace-expansion@>=1.0.0 <=1.1.11": ">=5.0.6", "brace-expansion@>=1.0.0 <=1.1.11": ">=5.0.5",
"esbuild@<=0.24.2": ">=0.28.0", "esbuild@<=0.24.2": ">=0.28.0",
"form-data@>=4.0.0 <4.0.4": ">=4.0.5", "form-data@>=4.0.0 <4.0.4": ">=4.0.5",
"hono@<4.6.5": ">=4.12.19", "hono@<4.6.5": ">=4.12.16",
"linkifyjs@<4.3.2": ">=4.3.3", "linkifyjs@<4.3.2": ">=4.3.2",
"nanoid@>=4.0.0 <5.0.9": ">=5.1.11", "nanoid@>=4.0.0 <5.0.9": ">=5.1.11",
"prismjs@<1.30.0": ">=1.30.0", "prismjs@<1.30.0": ">=1.30.0",
"proxmox-api>undici": "7.24.6", "proxmox-api>undici": "7.24.6",
"react-is": "^19.2.6", "react-is": "^19.2.5",
"rollup@>=4.0.0 <4.22.4": ">=4.60.4", "rollup@>=4.0.0 <4.22.4": ">=4.60.3",
"sha.js@<=2.4.11": ">=2.4.12", "sha.js@<=2.4.11": ">=2.4.12",
"tar-fs@>=3.0.0 <3.0.9": ">=3.1.2", "tar-fs@>=3.0.0 <3.0.9": ">=3.1.2",
"tar-fs@>=2.0.0 <2.1.3": ">=3.1.2", "tar-fs@>=2.0.0 <2.1.3": ">=3.1.2",
"tmp@<=0.2.3": ">=0.2.5", "tmp@<=0.2.3": ">=0.2.5",
"vite@>=5.0.0 <=5.4.18": ">=8.0.13" "vite@>=5.0.0 <=5.4.18": ">=8.0.10"
}, },
"patchedDependencies": { "patchedDependencies": {
"@types/node-unifi": "patches/@types__node-unifi.patch", "@types/node-unifi": "patches/@types__node-unifi.patch",
@@ -105,7 +104,6 @@
"allowUnusedPatches": true, "allowUnusedPatches": true,
"ignoredBuiltDependencies": [ "ignoredBuiltDependencies": [
"@scarf/scarf", "@scarf/scarf",
"@swc/core",
"core-js-pure", "core-js-pure",
"protobufjs" "protobufjs"
] ]
+1
View File
@@ -25,6 +25,7 @@
"@homarr/auth": "workspace:^0.1.0", "@homarr/auth": "workspace:^0.1.0",
"@homarr/common": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0",
"@homarr/core": "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-job-status": "workspace:^0.1.0",
"@homarr/cron-jobs": "workspace:^0.1.0", "@homarr/cron-jobs": "workspace:^0.1.0",
"@homarr/db": "workspace:^0.1.0", "@homarr/db": "workspace:^0.1.0",
+16 -19
View File
@@ -2,36 +2,33 @@ import { observable } from "@trpc/server/observable";
import z from "zod/v4"; import z from "zod/v4";
import { createLogger } from "@homarr/core/infrastructure/logs"; 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 type { TaskStatus } from "@homarr/cron-job-status";
import { createCronJobStatusChannel } 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"; import { createTRPCRouter, permissionRequiredProcedure } from "../trpc";
const jobGroupKeys = jobGroup.getKeys();
const jobNameSchema = z.enum(jobGroup.getKeys());
const logger = createLogger({ module: "cronJobsRouter" }); const logger = createLogger({ module: "cronJobsRouter" });
export const cronJobsRouter = createTRPCRouter({ export const cronJobsRouter = createTRPCRouter({
triggerJob: permissionRequiredProcedure triggerJob: permissionRequiredProcedure
.requiresPermission("admin") .requiresPermission("admin")
.input(jobNameSchema) .input(jobNameSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input }) => {
await new JobManager(ctx.db, jobGroup).triggerAsync(input); await cronJobApi.trigger.mutate(input);
}), }),
startJob: permissionRequiredProcedure startJob: permissionRequiredProcedure
.requiresPermission("admin") .requiresPermission("admin")
.input(jobNameSchema) .input(jobNameSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input }) => {
await new JobManager(ctx.db, jobGroup).startAsync(input); await cronJobApi.start.mutate(input);
}), }),
stopJob: permissionRequiredProcedure stopJob: permissionRequiredProcedure
.requiresPermission("admin") .requiresPermission("admin")
.input(jobNameSchema) .input(jobNameSchema)
.mutation(({ input, ctx }) => { .mutation(async ({ input }) => {
new JobManager(ctx.db, jobGroup).stop(input); await cronJobApi.stop.mutate(input);
}), }),
updateJobInterval: permissionRequiredProcedure updateJobInterval: permissionRequiredProcedure
.requiresPermission("admin") .requiresPermission("admin")
@@ -41,23 +38,23 @@ export const cronJobsRouter = createTRPCRouter({
cron: z.string(), cron: z.string(),
}), }),
) )
.mutation(async ({ input, ctx }) => { .mutation(async ({ input }) => {
await new JobManager(ctx.db, jobGroup).updateIntervalAsync(input.name, input.cron); await cronJobApi.updateInterval.mutate(input);
}), }),
disableJob: permissionRequiredProcedure disableJob: permissionRequiredProcedure
.requiresPermission("admin") .requiresPermission("admin")
.input(jobNameSchema) .input(jobNameSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input }) => {
await new JobManager(ctx.db, jobGroup).disableAsync(input); await cronJobApi.disable.mutate(input);
}), }),
enableJob: permissionRequiredProcedure enableJob: permissionRequiredProcedure
.requiresPermission("admin") .requiresPermission("admin")
.input(jobNameSchema) .input(jobNameSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input }) => {
await new JobManager(ctx.db, jobGroup).enableAsync(input); await cronJobApi.enable.mutate(input);
}), }),
getJobs: permissionRequiredProcedure.requiresPermission("admin").query(async ({ ctx }) => { getJobs: permissionRequiredProcedure.requiresPermission("admin").query(async () => {
return await new JobManager(ctx.db, jobGroup).getAllAsync(); return await cronJobApi.getAll.query();
}), }),
subscribeToStatusUpdates: permissionRequiredProcedure.requiresPermission("admin").subscription(() => { subscribeToStatusUpdates: permissionRequiredProcedure.requiresPermission("admin").subscription(() => {
return observable<TaskStatus>((emit) => { return observable<TaskStatus>((emit) => {
+1 -19
View File
@@ -1,6 +1,6 @@
import { describe, expect, test } from "vitest"; import { describe, expect, test } from "vitest";
import { getPortFromUrl, isAbsoluteUrl } from "../url.js"; import { getPortFromUrl } from "../url";
describe("getPortFromUrl", () => { describe("getPortFromUrl", () => {
test.each([ test.each([
@@ -38,21 +38,3 @@ describe("getPortFromUrl", () => {
expect(act).toThrowError("Unsupported protocol: ftp:"); expect(act).toThrowError("Unsupported protocol: ftp:");
}); });
}); });
describe("isAbsoluteUrl", () => {
test.each([
["http://example.com", true],
["https://example.com", true],
["ftp://example.com", true],
["file:///path/to/file", true],
["mailto:johndoe@example.com", true],
["/relative/path", false],
["relative/path", false],
])("should return %s for URL: %s", (urlString, expected) => {
// Act
const result = isAbsoluteUrl(urlString);
// Assert
expect(result).toBe(expected);
});
});
-6
View File
@@ -38,9 +38,3 @@ export const getPortFromUrl = (url: URL): number => {
throw new Error(`Unsupported protocol: ${url.protocol}`); throw new Error(`Unsupported protocol: ${url.protocol}`);
}; };
const absoluteUrlRegex = /^[a-z]+:(\/\/)?/;
export const isAbsoluteUrl = (urlOrPath: string): boolean => {
return absoluteUrlRegex.test(urlOrPath.toLowerCase());
};
+9
View File
@@ -0,0 +1,9 @@
import baseConfig from "@homarr/eslint-config/base";
/** @type {import('typescript-eslint').Config} */
export default [
{
ignores: [],
},
...baseConfig,
];
+46
View File
@@ -0,0 +1,46 @@
{
"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
@@ -0,0 +1,20 @@
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
@@ -0,0 +1,3 @@
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
@@ -0,0 +1,11 @@
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
@@ -0,0 +1,85 @@
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
@@ -0,0 +1,8 @@
{
"extends": "@homarr/tsconfig/base.json",
"compilerOptions": {
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
},
"include": ["*.ts", "src"],
"exclude": ["node_modules"]
}
+2 -4
View File
@@ -5,8 +5,7 @@
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
"exports": { "exports": {
".": "./index.ts", ".": "./index.ts"
"./job-manager": "./src/job-manager.ts"
}, },
"typesVersions": { "typesVersions": {
"*": { "*": {
@@ -39,8 +38,7 @@
"@homarr/server-settings": "workspace:^0.1.0", "@homarr/server-settings": "workspace:^0.1.0",
"@homarr/translation": "workspace:^0.1.0", "@homarr/translation": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0",
"semver-parser": "catalog:", "semver-parser": "catalog:"
"zod": "catalog:"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
+29 -40
View File
@@ -28,45 +28,34 @@ import { updateCheckerJob } from "./jobs/update-checker";
import { weatherJob } from "./jobs/weather"; import { weatherJob } from "./jobs/weather";
import { createCronJobGroup } from "./lib"; import { createCronJobGroup } from "./lib";
const getJobGroup = () => { export const jobGroup = createCronJobGroup({
return createCronJobGroup({ analytics: analyticsJob,
analytics: analyticsJob, iconsUpdater: iconsUpdaterJob,
iconsUpdater: iconsUpdaterJob, ping: pingJob,
ping: pingJob, smartHomeEntityState: smartHomeEntityStateJob,
smartHomeEntityState: smartHomeEntityStateJob, mediaServer: mediaServerJob,
mediaServer: mediaServerJob, mediaOrganizer: mediaOrganizerJob,
mediaOrganizer: mediaOrganizerJob, downloads: downloadsJob,
downloads: downloadsJob, dnsHole: dnsHoleJob,
dnsHole: dnsHoleJob, mediaRequestStats: mediaRequestStatsJob,
mediaRequestStats: mediaRequestStatsJob, mediaRequestList: mediaRequestListJob,
mediaRequestList: mediaRequestListJob, rssFeeds: rssFeedsJob,
rssFeeds: rssFeedsJob, indexerManager: indexerManagerJob,
indexerManager: indexerManagerJob, healthMonitoring: healthMonitoringJob,
healthMonitoring: healthMonitoringJob, updateChecker: updateCheckerJob,
updateChecker: updateCheckerJob, mediaTranscoding: mediaTranscodingJob,
mediaTranscoding: mediaTranscodingJob, minecraftServerStatus: minecraftServerStatusJob,
minecraftServerStatus: minecraftServerStatusJob, dockerContainers: dockerContainersJob,
dockerContainers: dockerContainersJob, networkController: networkControllerJob,
networkController: networkControllerJob, firewallCpu: firewallCpuJob,
firewallCpu: firewallCpuJob, firewallMemory: firewallMemoryJob,
firewallMemory: firewallMemoryJob, firewallVersion: firewallVersionJob,
firewallVersion: firewallVersionJob, firewallInterfaces: firewallInterfacesJob,
firewallInterfaces: firewallInterfacesJob, refreshNotifications: refreshNotificationsJob,
refreshNotifications: refreshNotificationsJob, weather: weatherJob,
weather: weatherJob, timetable: timetableJob,
timetable: timetableJob, tracearr: tracearrJob,
tracearr: tracearrJob, umamiActiveVisitors: umamiActiveVisitorsJob,
umamiActiveVisitors: umamiActiveVisitorsJob, });
});
};
// 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]; export type JobGroupKeys = ReturnType<(typeof jobGroup)["getKeys"]>[number];
+1 -87
View File
@@ -1,7 +1,6 @@
import { createId, objectKeys } from "@homarr/common"; import { createId, objectKeys } from "@homarr/common";
import { import {
createDocumentationLink, createDocumentationLink,
credentialsAdminGroup,
everyoneGroup, everyoneGroup,
getIntegrationDefaultUrl, getIntegrationDefaultUrl,
getIntegrationName, getIntegrationName,
@@ -12,24 +11,12 @@ import { defaultServerSettings, defaultServerSettingsKeys } from "@homarr/server
import type { Database } from ".."; import type { Database } from "..";
import { eq } from ".."; import { eq } from "..";
import { getMaxGroupPositionAsync } from "../queries";
import { import {
getServerSettingByKeyAsync, getServerSettingByKeyAsync,
insertServerSettingByKeyAsync, insertServerSettingByKeyAsync,
updateServerSettingByKeyAsync, updateServerSettingByKeyAsync,
} from "../queries/server-setting"; } from "../queries/server-setting";
import { import { groups, integrations, onboarding, searchEngines } from "../schema";
boards,
groupMembers,
groupPermissions,
groups,
integrations,
layouts,
onboarding,
searchEngines,
sections,
users,
} from "../schema";
import type { Integration } from "../schema"; import type { Integration } from "../schema";
export const seedDataAsync = async (db: Database) => { export const seedDataAsync = async (db: Database) => {
@@ -38,10 +25,6 @@ export const seedDataAsync = async (db: Database) => {
await seedServerSettingsAsync(db); await seedServerSettingsAsync(db);
await seedDefaultSearchEnginesAsync(db); await seedDefaultSearchEnginesAsync(db);
await seedDefaultIntegrationsAsync(db); await seedDefaultIntegrationsAsync(db);
if (process.env.DEMO_MODE === "true") {
await seedDemoUserAsync(db);
}
}; };
const seedEveryoneGroupAsync = async (db: Database) => { const seedEveryoneGroupAsync = async (db: Database) => {
@@ -208,72 +191,3 @@ const seedDefaultIntegrationsAsync = async (db: Database) => {
console.log(`Created ${createdCount} default integrations through seeding process`); console.log(`Created ${createdCount} default integrations through seeding process`);
}; };
const seedDemoUserAsync = async (db: Database) => {
const existingUser = await db.query.users.findFirst({
where: eq(users.name, "demo"),
});
if (existingUser) {
console.log("Skipping seeding of demo user as it already exists");
return;
}
const userId = createId();
await db.insert(users).values({
id: userId,
name: "demo",
email: "demo@example.com",
password: "$2b$10$c5AaQb0oj32/bui5Z55PyOb7pY.iEkXYqgdD8MjS9LjSjLMeOkZu2",
});
const maxPosition = await getMaxGroupPositionAsync(db);
const groupId = createId();
await db.insert(groups).values({
id: groupId,
name: credentialsAdminGroup,
ownerId: userId,
position: maxPosition + 1,
});
await db.insert(groupPermissions).values({
groupId,
permission: "admin",
});
await db.insert(groupMembers).values({
groupId,
userId,
});
await db.update(onboarding).set({
step: "finish",
previousStep: "settings",
});
const boardId = createId();
await db.insert(boards).values({
id: boardId,
name: "default",
isPublic: false,
creatorId: userId,
});
await db.insert(sections).values({
id: createId(),
kind: "empty",
xOffset: 0,
yOffset: 0,
boardId,
});
await db.insert(layouts).values({
id: createId(),
name: "Base",
columnCount: 12,
breakpoint: 0,
boardId,
});
await db.update(users).set({ homeBoardId: boardId }).where(eq(users.id, userId));
console.log(
"Your instance has enabled demo mode. Homarr therefore created example data for your demo. You can disable demo mode anytime by setting the env 'DEMO_MODE' to 'false' or unset it.",
);
};
@@ -1,4 +1,3 @@
import { isAbsoluteUrl } from "@homarr/common";
import { ResponseError } from "@homarr/common/server"; import { ResponseError } from "@homarr/common/server";
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
import { ImageProxy } from "@homarr/image-proxy"; import { ImageProxy } from "@homarr/image-proxy";
@@ -86,10 +85,10 @@ export class TracearrIntegration extends Integration {
data: await Promise.all( data: await Promise.all(
json.data.map(async (stream) => { json.data.map(async (stream) => {
if (stream.userAvatarUrl) { if (stream.userAvatarUrl) {
stream.userAvatarUrl = await this.proxyImageAsync(imageProxy, stream.userAvatarUrl); stream.userAvatarUrl = await this.proxyImageAsync(imageProxy, stream.userAvatarUrl, stream.id, "avatar");
} }
if (stream.posterUrl) { if (stream.posterUrl) {
stream.posterUrl = await this.proxyImageAsync(imageProxy, stream.posterUrl); stream.posterUrl = await this.proxyImageAsync(imageProxy, stream.posterUrl, stream.id, "poster");
} }
return stream; return stream;
}), }),
@@ -121,7 +120,12 @@ export class TracearrIntegration extends Integration {
...json, ...json,
data: await Promise.all( data: await Promise.all(
json.data.map(async (violation) => { json.data.map(async (violation) => {
violation.user.avatarUrl = await this.proxyImageAsync(imageProxy, violation.user.avatarUrl); violation.user.avatarUrl = await this.proxyImageAsync(
imageProxy,
violation.user.avatarUrl,
violation.user.id,
"avatar",
);
return violation; return violation;
}), }),
), ),
@@ -152,7 +156,12 @@ export class TracearrIntegration extends Integration {
...json, ...json,
data: await Promise.all( data: await Promise.all(
json.data.map(async (session) => { json.data.map(async (session) => {
session.user.avatarUrl = await this.proxyImageAsync(imageProxy, session.user.avatarUrl); session.user.avatarUrl = await this.proxyImageAsync(
imageProxy,
session.user.avatarUrl,
session.user.id,
"avatar",
);
return session; return session;
}), }),
), ),
@@ -185,12 +194,23 @@ export class TracearrIntegration extends Integration {
}; };
} }
private async proxyImageAsync(imageProxy: ImageProxy, urlOrPath: string | null | undefined): Promise<string | null> { private async proxyImageAsync(
if (!urlOrPath) return null; imageProxy: ImageProxy,
url: string | null | undefined,
uniqueId: string,
discriminator: string,
): Promise<string | null> {
if (!url) return null;
try { try {
// ImageProxy doesn't support fallback urls so we need to remove them // ImageProxy doesn't support fallback urls so we need to remove them
const cleanUrl = urlOrPath.replace(/&fallback=[^&]+/, "").replace(/\?fallback=[^&]+&?/, "?"); const cleanUrl = url.replace(/&fallback=[^&]+/, "").replace(/\?fallback=[^&]+&?/, "?");
const fullUrl = isAbsoluteUrl(urlOrPath) ? cleanUrl : this.url(cleanUrl as `/${string}`).toString(); // Build the full URL, then inject _uid as the FIRST query param.
// This is critical because ImageProxy uses bcrypt which truncates at 72 bytes.
// Without this, all long avatar/poster URLs from the same session hash identically
// since they share the same first 72+ bytes.
const baseUrl = this.url(cleanUrl as `/${string}`).toString();
const prefix = `_uid=${discriminator}_${uniqueId}&`;
const fullUrl = baseUrl.includes("?") ? baseUrl.replace("?", `?${prefix}`) : `${baseUrl}?${prefix}`;
return await imageProxy.createImageAsync(fullUrl, this.getAuthHeaders()); return await imageProxy.createImageAsync(fullUrl, this.getAuthHeaders());
} catch { } catch {
+2277 -1203
View File
File diff suppressed because it is too large. Load diff
+45 -47
View File
@@ -13,7 +13,7 @@ catalog:
"@dnd-kit/sortable": ^10.0.0 "@dnd-kit/sortable": ^10.0.0
"@dnd-kit/utilities": ^3.2.2 "@dnd-kit/utilities": ^3.2.2
"@drizzle-team/brocli": ^0.12.0 "@drizzle-team/brocli": ^0.12.0
"@eslint/js": 10.0.1 '@eslint/js': 10.0.1
"@extractus/feed-extractor": 7.2.1 "@extractus/feed-extractor": 7.2.1
"@gitbeaker/rest": ^43.8.0 "@gitbeaker/rest": ^43.8.0
"@homarr/gridstack": ^1.12.0 "@homarr/gridstack": ^1.12.0
@@ -33,46 +33,46 @@ catalog:
"@mantine/notifications": ^8.3.18 "@mantine/notifications": ^8.3.18
"@mantine/spotlight": ^8.3.18 "@mantine/spotlight": ^8.3.18
"@mantine/tiptap": ^8.3.18 "@mantine/tiptap": ^8.3.18
"@next/eslint-plugin-next": 16.2.6 "@million/lint": 1.0.14
"@next/eslint-plugin-next": 16.2.4
"@octokit/auth-app": ^8.2.0 "@octokit/auth-app": ^8.2.0
"@paralleldrive/cuid2": ^3.3.0 "@paralleldrive/cuid2": ^3.3.0
"@playwright/test": ^1.60.0
"@semantic-release/changelog": ^6.0.3 "@semantic-release/changelog": ^6.0.3
"@semantic-release/commit-analyzer": ^13.0.1 "@semantic-release/commit-analyzer": ^13.0.1
"@semantic-release/git": ^10.0.1 "@semantic-release/git": ^10.0.1
"@semantic-release/github": ^12.0.8 "@semantic-release/github": ^12.0.6
"@semantic-release/npm": ^13.1.5 "@semantic-release/npm": ^13.1.5
"@semantic-release/release-notes-generator": ^14.1.1 "@semantic-release/release-notes-generator": ^14.1.0
"@t3-oss/env-nextjs": ^0.13.11 "@t3-oss/env-nextjs": ^0.13.11
"@tabler/icons-react": ^3.44.0 "@tabler/icons-react": ^3.42.0
"@tanstack/react-query": ^5.100.10 "@tanstack/react-query": ^5.100.9
"@tanstack/react-query-devtools": ^5.100.10 "@tanstack/react-query-devtools": ^5.100.9
"@tanstack/react-query-next-experimental": ^5.100.10 "@tanstack/react-query-next-experimental": ^5.100.9
"@testcontainers/mysql": ^11.14.0 "@testcontainers/mysql": ^11.14.0
"@testcontainers/postgresql": ^11.14.0 "@testcontainers/postgresql": ^11.14.0
"@testcontainers/redis": ^11.14.0 "@testcontainers/redis": ^11.14.0
"@tiptap/extension-color": 3.23.4 "@tiptap/extension-color": 3.22.5
"@tiptap/extension-highlight": 3.23.4 "@tiptap/extension-highlight": 3.22.5
"@tiptap/extension-image": 3.23.4 "@tiptap/extension-image": 3.22.5
"@tiptap/extension-link": ^3.23.4 "@tiptap/extension-link": ^3.22.5
"@tiptap/extension-placeholder": ^3.23.4 "@tiptap/extension-placeholder": ^3.22.5
"@tiptap/extension-table": 3.23.4 "@tiptap/extension-table": 3.22.5
"@tiptap/extension-table-cell": 3.23.4 "@tiptap/extension-table-cell": 3.22.5
"@tiptap/extension-table-header": 3.23.4 "@tiptap/extension-table-header": 3.22.5
"@tiptap/extension-table-row": 3.23.4 "@tiptap/extension-table-row": 3.22.5
"@tiptap/extension-task-item": 3.23.4 "@tiptap/extension-task-item": 3.22.5
"@tiptap/extension-task-list": 3.23.4 "@tiptap/extension-task-list": 3.22.5
"@tiptap/extension-text-align": 3.23.4 "@tiptap/extension-text-align": 3.22.5
"@tiptap/extension-text-style": 3.23.4 "@tiptap/extension-text-style": 3.22.5
"@tiptap/extension-underline": 3.23.4 "@tiptap/extension-underline": 3.22.5
"@tiptap/react": ^3.23.4 "@tiptap/react": ^3.22.5
"@tiptap/starter-kit": ^3.23.4 "@tiptap/starter-kit": ^3.22.5
"@trpc/client": ^11.17.0 "@trpc/client": ^11.17.0
"@trpc/next": ^11.17.0 "@trpc/next": ^11.17.0
"@trpc/react-query": ^11.17.0 "@trpc/react-query": ^11.17.0
"@trpc/server": ^11.17.0 "@trpc/server": ^11.17.0
"@trpc/tanstack-react-query": ^11.17.0 "@trpc/tanstack-react-query": ^11.17.0
"@turbo/gen": ^2.9.14 "@turbo/gen": ^2.9.9
"@types/adm-zip": 0.5.8 "@types/adm-zip": 0.5.8
"@types/bcrypt": 6.0.0 "@types/bcrypt": 6.0.0
"@types/better-sqlite3": 7.6.13 "@types/better-sqlite3": 7.6.13
@@ -81,7 +81,7 @@ catalog:
"@types/crypto-js": ^4.2.2 "@types/crypto-js": ^4.2.2
"@types/css-modules": ^1.0.5 "@types/css-modules": ^1.0.5
"@types/dockerode": ^4.0.1 "@types/dockerode": ^4.0.1
"@types/node": ^24.12.4 "@types/node": ^24.12.2
"@types/node-unifi": ^2.5.1 "@types/node-unifi": ^2.5.1
"@types/pg": ^8.20.0 "@types/pg": ^8.20.0
"@types/prismjs": ^1.26.6 "@types/prismjs": ^1.26.6
@@ -93,15 +93,15 @@ catalog:
"@types/xml2js": ^0.4.14 "@types/xml2js": ^0.4.14
"@umami/node": ^0.4.0 "@umami/node": ^0.4.0
"@vitejs/plugin-react": ^5.2.0 "@vitejs/plugin-react": ^5.2.0
"@vitest/coverage-v8": ^4.1.6 "@vitest/coverage-v8": ^4.1.5
"@vitest/ui": ^4.1.6 "@vitest/ui": ^4.1.5
"@xterm/addon-canvas": ^0.7.0 "@xterm/addon-canvas": ^0.7.0
"@xterm/addon-fit": 0.11.0 "@xterm/addon-fit": 0.11.0
"@xterm/xterm": ^6.0.0 "@xterm/xterm": ^6.0.0
adm-zip: 0.5.17 adm-zip: 0.5.17
babel-plugin-react-compiler: ^1.0.0 babel-plugin-react-compiler: ^1.0.0
bcrypt: ^6.0.0 bcrypt: ^6.0.0
better-sqlite3: ^12.10.0 better-sqlite3: ^12.9.0
chroma-js: ^3.2.0 chroma-js: ^3.2.0
clsx: ^2.1.1 clsx: ^2.1.1
concurrently: ^9.2.1 concurrently: ^9.2.1
@@ -121,21 +121,21 @@ catalog:
drizzle-orm: ^0.45.2 drizzle-orm: ^0.45.2
drizzle-zod: ^0.8.3 drizzle-zod: ^0.8.3
esbuild: ^0.28.0 esbuild: ^0.28.0
eslint: ^10.4.0 eslint: ^10.3.0
eslint-config-prettier: ^10.1.8 eslint-config-prettier: ^10.1.8
eslint-config-turbo: ^2.9.14 eslint-config-turbo: ^2.9.9
eslint-plugin-import: ^2.32.0 eslint-plugin-import: ^2.32.0
eslint-plugin-jsx-a11y: ^6.10.2 eslint-plugin-jsx-a11y: ^6.10.2
eslint-plugin-react: ^7.37.5 eslint-plugin-react: ^7.37.5
eslint-plugin-react-hooks: ^6.1.1 eslint-plugin-react-hooks: ^6.1.1
fast-xml-parser: ^5.8.0 fast-xml-parser: ^5.7.3
fastify: ^5.8.5 fastify: ^5.8.5
flag-icons: ^7.5.0 flag-icons: ^7.5.0
glob: ^13.0.6 glob: ^13.0.6
ical.js: ^2.2.1 ical.js: ^2.2.1
ioredis: 5.10.1 ioredis: 5.10.1
isomorphic-dompurify: ^3.13.0 isomorphic-dompurify: ^3.12.0
jotai: ^2.20.0 jotai: ^2.19.1
jsdom: ^29.1.1 jsdom: ^29.1.1
json5: ^2.2.3 json5: ^2.2.3
ldapts: 8.1.7 ldapts: 8.1.7
@@ -144,9 +144,9 @@ catalog:
mantine-react-table: 2.0.0-beta.9 mantine-react-table: 2.0.0-beta.9
maria2: ^0.4.1 maria2: ^0.4.1
mysql2: 3.22.3 mysql2: 3.22.3
next: 16.2.6 next: 16.2.4
next-auth: 5.0.0-beta.31 next-auth: 5.0.0-beta.31
next-intl: 4.12.0 next-intl: 4.11.0
node-loader: ^2.1.0 node-loader: ^2.1.0
octokit: ^5.0.5 octokit: ^5.0.5
pg: ^8.20.0 pg: ^8.20.0
@@ -155,8 +155,8 @@ catalog:
prettier-plugin-packagejson: ^3.0.2 prettier-plugin-packagejson: ^3.0.2
prismjs: ^1.30.0 prismjs: ^1.30.0
proxmox-api: 1.1.1 proxmox-api: 1.1.1
react: 19.2.6 react: 19.2.5
react-dom: 19.2.6 react-dom: 19.2.5
react-error-boundary: ^6.1.1 react-error-boundary: ^6.1.1
react-markdown: ^10.1.0 react-markdown: ^10.1.0
react-quill-new: ^3.8.3 react-quill-new: ^3.8.3
@@ -167,24 +167,24 @@ catalog:
semver-parser: ^4.1.8 semver-parser: ^4.1.8
superjson: 2.2.6 superjson: 2.2.6
svgson: ^5.3.1 svgson: ^5.3.1
swagger-ui-react: ^5.32.6 swagger-ui-react: ^5.32.5
testcontainers: ^11.14.0 testcontainers: ^11.14.0
timezones-ical-library: ^2.2.0 timezones-ical-library: ^2.2.0
trpc-to-openapi: ^3.2.0 trpc-to-openapi: ^3.2.0
tsdav: ^2.2.2 tsdav: ^2.2.0
tsx: 4.20.4 tsx: 4.20.4
turbo: ^2.9.14 turbo: ^2.9.9
typescript: ^5.9.3 typescript: ^5.9.3
typescript-eslint: ^8.58.2 typescript-eslint: ^8.58.2
undici: 7.24.6 undici: 7.24.6
use-deep-compare-effect: ^1.8.1 use-deep-compare-effect: ^1.8.1
video.js: ^8.23.7 video.js: ^8.23.7
vite-tsconfig-paths: ^6.1.1 vite-tsconfig-paths: ^6.1.1
vitest: ^4.1.6 vitest: ^4.1.5
winston: 3.19.0 winston: 3.19.0
ws: ^8.20.1 ws: ^8.20.0
xml2js: ^0.6.2 xml2js: ^0.6.2
yaml: 2.9.0 yaml: 2.8.4
zod: ^4.4.3 zod: ^4.4.3
zod-form-data: ^3.0.1 zod-form-data: ^3.0.1
zod-validation-error: ^5.0.0 zod-validation-error: ^5.0.0
@@ -193,5 +193,3 @@ minimumReleaseAge: 4320
minimumReleaseAgeExclude: minimumReleaseAgeExclude:
- next - next
- "@next/*" - "@next/*"
# Renovate security update: turbo@2.9.14
- turbo@2.9.14
+11 -1
View File
@@ -16,6 +16,8 @@ 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 # 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) 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)
# Start nginx proxy # Start nginx proxy
# 1. Replace the HOSTNAME in the nginx template file # 1. Replace the HOSTNAME in the nginx template file
@@ -36,13 +38,21 @@ else
REDIS_PID=$! REDIS_PID=$!
fi fi
node apps/tasks/tasks.cjs &
TASKS_PID=$!
node apps/websocket/wssServer.cjs &
WSS_PID=$!
node apps/nextjs/server.js & node apps/nextjs/server.js &
NEXTJS_PID=$! NEXTJS_PID=$!
# Function to handle SIGTERM and shut down services # Function to handle SIGTERM and shut down services
terminate() { terminate() {
echo "Received SIGTERM. Shutting down..." echo "Received SIGTERM. Shutting down..."
kill -TERM $NGINX_PID $NEXTJS_PID 2>/dev/null kill -TERM $NGINX_PID $TASKS_PID $WSS_PID $NEXTJS_PID 2>/dev/null
wait wait
# kill redis-server last because of logging of other services and only if $REDIS_PID is set # kill redis-server last because of logging of other services and only if $REDIS_PID is set
if [ -n "$REDIS_PID" ]; then if [ -n "$REDIS_PID" ]; then
+1 -1
View File
@@ -1 +1 @@
[{"login":"ajnart","avatar_url":"https://avatars.githubusercontent.com/u/49837342?v=4"},{"login":"manuel-rw","avatar_url":"https://avatars.githubusercontent.com/u/30572287?v=4"},{"login":"Meierschlumpf","avatar_url":"https://avatars.githubusercontent.com/u/63781622?v=4"},{"login":"SeDemal","avatar_url":"https://avatars.githubusercontent.com/u/26098587?v=4"},{"login":"lammersbjorn","avatar_url":"https://avatars.githubusercontent.com/u/71191962?v=4"},{"login":"LarveyOfficial","avatar_url":"https://avatars.githubusercontent.com/u/39219859?v=4"},{"login":"c00ldude1oo","avatar_url":"https://avatars.githubusercontent.com/u/15677803?v=4"},{"login":"InterN0te","avatar_url":"https://avatars.githubusercontent.com/u/10882916?v=4"},{"login":"Aimsucks","avatar_url":"https://avatars.githubusercontent.com/u/1828486?v=4"},{"login":"JannesV","avatar_url":"https://avatars.githubusercontent.com/u/5984244?v=4"},{"login":"MauriceNino","avatar_url":"https://avatars.githubusercontent.com/u/18658092?v=4"},{"login":"hillaliy","avatar_url":"https://avatars.githubusercontent.com/u/950010?v=4"},{"login":"Skeeww","avatar_url":"https://avatars.githubusercontent.com/u/9639992?v=4"},{"login":"RichyHBM","avatar_url":"https://avatars.githubusercontent.com/u/5472275?v=4"},{"login":"Bonfire","avatar_url":"https://avatars.githubusercontent.com/u/5704760?v=4"},{"login":"wieli99","avatar_url":"https://avatars.githubusercontent.com/u/46794825?v=4"},{"login":"ishaanparlikar","avatar_url":"https://avatars.githubusercontent.com/u/43013132?v=4"},{"login":"Zareix","avatar_url":"https://avatars.githubusercontent.com/u/29818713?v=4"},{"login":"tuggan","avatar_url":"https://avatars.githubusercontent.com/u/4325608?v=4"},{"login":"Spillebulle","avatar_url":"https://avatars.githubusercontent.com/u/46653946?v=4"},{"login":"spkesDE","avatar_url":"https://avatars.githubusercontent.com/u/1682314?v=4"},{"login":"j3lte","avatar_url":"https://avatars.githubusercontent.com/u/2557568?v=4"},{"login":"dslatt","avatar_url":"https://avatars.githubusercontent.com/u/5211001?v=4"},{"login":"Malong11-007","avatar_url":"https://avatars.githubusercontent.com/u/40298510?v=4"},{"login":"StefanB7","avatar_url":"https://avatars.githubusercontent.com/u/22056509?v=4"},{"login":"souravghosh01","avatar_url":"https://avatars.githubusercontent.com/u/37548139?v=4"},{"login":"SR3u","avatar_url":"https://avatars.githubusercontent.com/u/2380522?v=4"},{"login":"Rikpat","avatar_url":"https://avatars.githubusercontent.com/u/33869814?v=4"},{"login":"NormanJS","avatar_url":"https://avatars.githubusercontent.com/u/83409549?v=4"},{"login":"No-Maines-Land","avatar_url":"https://avatars.githubusercontent.com/u/20687725?v=4"},{"login":"stark1tty","avatar_url":"https://avatars.githubusercontent.com/u/69326376?v=4"},{"login":"Momcilo42","avatar_url":"https://avatars.githubusercontent.com/u/10118137?v=4"},{"login":"marius-arch","avatar_url":"https://avatars.githubusercontent.com/u/72494359?v=4"},{"login":"Lumilias","avatar_url":"https://avatars.githubusercontent.com/u/10852161?v=4"},{"login":"Havock94","avatar_url":"https://avatars.githubusercontent.com/u/7635248?v=4"},{"login":"Lollyde","avatar_url":"https://avatars.githubusercontent.com/u/11347524?v=4"},{"login":"krishnamuppaneni","avatar_url":"https://avatars.githubusercontent.com/u/4703366?v=4"},{"login":"devtobi","avatar_url":"https://avatars.githubusercontent.com/u/28538704?v=4"},{"login":"TyxTang","avatar_url":"https://avatars.githubusercontent.com/u/90897955?v=4"},{"login":"alotlikebeans","avatar_url":"https://avatars.githubusercontent.com/u/19630511?v=4"},{"login":"WillyJL","avatar_url":"https://avatars.githubusercontent.com/u/49810075?v=4"},{"login":"anonysoul","avatar_url":"https://avatars.githubusercontent.com/u/105715150?v=4"},{"login":"cospeedster","avatar_url":"https://avatars.githubusercontent.com/u/52749645?v=4"},{"login":"diederbert","avatar_url":"https://avatars.githubusercontent.com/u/162878798?v=4"},{"login":"digdilem","avatar_url":"https://avatars.githubusercontent.com/u/4052275?v=4"},{"login":"dinghino","avatar_url":"https://avatars.githubusercontent.com/u/14729459?v=4"},{"login":"gfomichev","avatar_url":"https://avatars.githubusercontent.com/u/1596146?v=4"},{"login":"gnattu","avatar_url":"https://avatars.githubusercontent.com/u/12995396?v=4"},{"login":"tuxpizza","avatar_url":"https://avatars.githubusercontent.com/u/84710786?v=4"},{"login":"xmarkurion","avatar_url":"https://avatars.githubusercontent.com/u/46654687?v=4"},{"login":"huanga","avatar_url":"https://avatars.githubusercontent.com/u/732970?v=4"},{"login":"taos15","avatar_url":"https://avatars.githubusercontent.com/u/122130728?v=4"},{"login":"arghyadipchak","avatar_url":"https://avatars.githubusercontent.com/u/32430374?v=4"},{"login":"AuthorShin","avatar_url":"https://avatars.githubusercontent.com/u/4959043?v=4"},{"login":"benniblot","avatar_url":"https://avatars.githubusercontent.com/u/47495914?v=4"},{"login":"bebsworthy","avatar_url":"https://avatars.githubusercontent.cLine truncated [{"login":"ajnart","avatar_url":"https://avatars.githubusercontent.com/u/49837342?v=4"},{"login":"manuel-rw","avatar_url":"https://avatars.githubusercontent.com/u/30572287?v=4"},{"login":"Meierschlumpf","avatar_url":"https://avatars.githubusercontent.com/u/63781622?v=4"},{"login":"SeDemal","avatar_url":"https://avatars.githubusercontent.com/u/26098587?v=4"},{"login":"lammersbjorn","avatar_url":"https://avatars.githubusercontent.com/u/71191962?v=4"},{"login":"LarveyOfficial","avatar_url":"https://avatars.githubusercontent.com/u/39219859?v=4"},{"login":"c00ldude1oo","avatar_url":"https://avatars.githubusercontent.com/u/15677803?v=4"},{"login":"InterN0te","avatar_url":"https://avatars.githubusercontent.com/u/10882916?v=4"},{"login":"Aimsucks","avatar_url":"https://avatars.githubusercontent.com/u/1828486?v=4"},{"login":"JannesV","avatar_url":"https://avatars.githubusercontent.com/u/5984244?v=4"},{"login":"MauriceNino","avatar_url":"https://avatars.githubusercontent.com/u/18658092?v=4"},{"login":"hillaliy","avatar_url":"https://avatars.githubusercontent.com/u/950010?v=4"},{"login":"Skeeww","avatar_url":"https://avatars.githubusercontent.com/u/9639992?v=4"},{"login":"RichyHBM","avatar_url":"https://avatars.githubusercontent.com/u/5472275?v=4"},{"login":"Bonfire","avatar_url":"https://avatars.githubusercontent.com/u/5704760?v=4"},{"login":"wieli99","avatar_url":"https://avatars.githubusercontent.com/u/46794825?v=4"},{"login":"ishaanparlikar","avatar_url":"https://avatars.githubusercontent.com/u/43013132?v=4"},{"login":"Zareix","avatar_url":"https://avatars.githubusercontent.com/u/29818713?v=4"},{"login":"tuggan","avatar_url":"https://avatars.githubusercontent.com/u/4325608?v=4"},{"login":"Spillebulle","avatar_url":"https://avatars.githubusercontent.com/u/46653946?v=4"},{"login":"spkesDE","avatar_url":"https://avatars.githubusercontent.com/u/1682314?v=4"},{"login":"j3lte","avatar_url":"https://avatars.githubusercontent.com/u/2557568?v=4"},{"login":"dslatt","avatar_url":"https://avatars.githubusercontent.com/u/5211001?v=4"},{"login":"Malong11-007","avatar_url":"https://avatars.githubusercontent.com/u/40298510?v=4"},{"login":"StefanB7","avatar_url":"https://avatars.githubusercontent.com/u/22056509?v=4"},{"login":"souravghosh01","avatar_url":"https://avatars.githubusercontent.com/u/37548139?v=4"},{"login":"SR3u","avatar_url":"https://avatars.githubusercontent.com/u/2380522?v=4"},{"login":"Rikpat","avatar_url":"https://avatars.githubusercontent.com/u/33869814?v=4"},{"login":"NormanJS","avatar_url":"https://avatars.githubusercontent.com/u/83409549?v=4"},{"login":"No-Maines-Land","avatar_url":"https://avatars.githubusercontent.com/u/20687725?v=4"},{"login":"stark1tty","avatar_url":"https://avatars.githubusercontent.com/u/69326376?v=4"},{"login":"Momcilo42","avatar_url":"https://avatars.githubusercontent.com/u/10118137?v=4"},{"login":"marius-arch","avatar_url":"https://avatars.githubusercontent.com/u/72494359?v=4"},{"login":"Lumilias","avatar_url":"https://avatars.githubusercontent.com/u/10852161?v=4"},{"login":"Havock94","avatar_url":"https://avatars.githubusercontent.com/u/7635248?v=4"},{"login":"Lollyde","avatar_url":"https://avatars.githubusercontent.com/u/11347524?v=4"},{"login":"krishnamuppaneni","avatar_url":"https://avatars.githubusercontent.com/u/4703366?v=4"},{"login":"devtobi","avatar_url":"https://avatars.githubusercontent.com/u/28538704?v=4"},{"login":"TyxTang","avatar_url":"https://avatars.githubusercontent.com/u/90897955?v=4"},{"login":"alotlikebeans","avatar_url":"https://avatars.githubusercontent.com/u/19630511?v=4"},{"login":"WillyJL","avatar_url":"https://avatars.githubusercontent.com/u/49810075?v=4"},{"login":"anonysoul","avatar_url":"https://avatars.githubusercontent.com/u/105715150?v=4"},{"login":"cospeedster","avatar_url":"https://avatars.githubusercontent.com/u/52749645?v=4"},{"login":"diederbert","avatar_url":"https://avatars.githubusercontent.com/u/162878798?v=4"},{"login":"digdilem","avatar_url":"https://avatars.githubusercontent.com/u/4052275?v=4"},{"login":"dinghino","avatar_url":"https://avatars.githubusercontent.com/u/14729459?v=4"},{"login":"gfomichev","avatar_url":"https://avatars.githubusercontent.com/u/1596146?v=4"},{"login":"gnattu","avatar_url":"https://avatars.githubusercontent.com/u/12995396?v=4"},{"login":"tuxpizza","avatar_url":"https://avatars.githubusercontent.com/u/84710786?v=4"},{"login":"xmarkurion","avatar_url":"https://avatars.githubusercontent.com/u/46654687?v=4"},{"login":"huanga","avatar_url":"https://avatars.githubusercontent.com/u/732970?v=4"},{"login":"taos15","avatar_url":"https://avatars.githubusercontent.com/u/122130728?v=4"},{"login":"arghyadipchak","avatar_url":"https://avatars.githubusercontent.com/u/32430374?v=4"},{"login":"AuthorShin","avatar_url":"https://avatars.githubusercontent.com/u/4959043?v=4"},{"login":"benniblot","avatar_url":"https://avatars.githubusercontent.com/u/47495914?v=4"},{"login":"bebsworthy","avatar_url":"https://avatars.githubusercontent.cLine truncated
+46 -11
View File
@@ -1,19 +1,57 @@
{ {
"$schema": "https://turborepo.org/schema.json", "$schema": "https://turborepo.org/schema.json",
"remoteCache": { "signature": false }, "globalDependencies": ["**/.env"],
"globalEnv": ["CI", "NODE_ENV", "SKIP_ENV_VALIDATION"], "globalEnv": [
"AUTH_LDAP_BASE",
"AUTH_LDAP_BIND_DN",
"AUTH_LDAP_BIND_PASSWORD",
"AUTH_LDAP_GROUP_CLASS",
"AUTH_LDAP_GROUP_FILTER_EXTRA_ARG",
"AUTH_LDAP_GROUP_MEMBER_ATTRIBUTE",
"AUTH_LDAP_GROUP_MEMBER_USER_ATTRIBUTE",
"AUTH_LDAP_SEARCH_SCOPE",
"AUTH_LDAP_URI",
"AUTH_OIDC_CLIENT_ID",
"AUTH_OIDC_CLIENT_NAME",
"AUTH_OIDC_CLIENT_SECRET",
"AUTH_OIDC_FORCE_USERINFO",
"AUTH_OIDC_ISSUER",
"AUTH_OIDC_SCOPE_OVERWRITE",
"AUTH_OIDC_GROUPS_ATTRIBUTE",
"AUTH_OIDC_NAME_ATTRIBUTE_OVERWRITE",
"AUTH_LDAP_USERNAME_ATTRIBUTE",
"AUTH_LDAP_USER_MAIL_ATTRIBUTE",
"AUTH_LDAP_USERNAME_FILTER_EXTRA_ARG",
"AUTH_OIDC_AUTO_LOGIN",
"AUTH_LOGOUT_REDIRECT_URL",
"AUTH_PROVIDERS",
"AUTH_SESSION_EXPIRY_TIME",
"CI",
"DISABLE_REDIS_LOGS",
"DB_URL",
"DB_HOST",
"DB_USER",
"DB_PASSWORD",
"DB_NAME",
"DB_PORT",
"DB_DRIVER",
"DOCKER_HOSTNAMES",
"DOCKER_PORTS",
"DOCKER_SOCKET_PATHS",
"NODE_ENV",
"PORT",
"LOCAL_CERTIFICATE_PATH",
"LOG_LEVEL",
"SECRET_ENCRYPTION_KEY",
"SKIP_ENV_VALIDATION"
],
"ui": "stream", "ui": "stream",
"futureFlags": {
"affectedUsingTaskInputs": true
},
"tasks": { "tasks": {
"topo": { "topo": {
"dependsOn": ["^topo"], "dependsOn": ["^topo"]
"inputs": ["package.json"]
}, },
"build": { "build": {
"dependsOn": ["^build"], "dependsOn": ["^build"],
"inputs": ["$TURBO_DEFAULT$", "!**/*.test.ts", "!**/*.test.tsx", "!**/*.spec.ts", "!**/__tests__/**"],
"outputs": [".next/**", "!.next/cache/**", "next-env.d.ts", ".output/**", ".vercel/output/**"] "outputs": [".next/**", "!.next/cache/**", "next-env.d.ts", ".output/**", ".vercel/output/**"]
}, },
"dev": { "dev": {
@@ -21,18 +59,15 @@
"cache": false "cache": false
}, },
"format": { "format": {
"inputs": ["$TURBO_DEFAULT$"],
"outputs": ["node_modules/.cache/.prettiercache"], "outputs": ["node_modules/.cache/.prettiercache"],
"outputLogs": "new-only" "outputLogs": "new-only"
}, },
"lint": { "lint": {
"dependsOn": ["^topo"], "dependsOn": ["^topo"],
"inputs": ["$TURBO_DEFAULT$"],
"outputs": ["node_modules/.cache/.eslintcache"] "outputs": ["node_modules/.cache/.eslintcache"]
}, },
"typecheck": { "typecheck": {
"dependsOn": ["^topo"], "dependsOn": ["^topo"],
"inputs": ["$TURBO_DEFAULT$"],
"outputs": ["node_modules/.cache/tsbuildinfo.json"] "outputs": ["node_modules/.cache/tsbuildinfo.json"]
}, },
"clean": { "clean": {