diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 3a39ba344..11004a3ef 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -31,6 +31,7 @@ body: label: Version description: What version of Homarr are you running? options: + - 1.29.0 - 1.28.1 - 1.28.0 - 1.27.0 diff --git a/Dockerfile b/Dockerfile index cd7f063e4..5cc126cec 100644 --- a/Dockerfile +++ b/Dockerfile @@ -64,6 +64,7 @@ ENV DB_URL='/appdata/db/db.sqlite' ENV DB_DIALECT='sqlite' ENV DB_DRIVER='better-sqlite3' ENV AUTH_PROVIDERS='credentials' +ENV REDIS_IS_EXTERNAL='false' ENV NODE_ENV='production' ENTRYPOINT [ "/app/entrypoint.sh" ] diff --git a/apps/nextjs/package.json b/apps/nextjs/package.json index 26b998837..4add7f6f2 100644 --- a/apps/nextjs/package.json +++ b/apps/nextjs/package.json @@ -25,15 +25,16 @@ "@homarr/boards": "workspace:^0.1.0", "@homarr/certificates": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0", + "@homarr/core": "workspace:^0.1.0", "@homarr/cron-job-status": "workspace:^0.1.0", "@homarr/db": "workspace:^0.1.0", "@homarr/definitions": "workspace:^0.1.0", "@homarr/docker": "workspace:^0.1.0", - "@homarr/env": "workspace:^0.1.0", "@homarr/form": "workspace:^0.1.0", "@homarr/forms-collection": "workspace:^0.1.0", "@homarr/gridstack": "^1.12.0", "@homarr/icons": "workspace:^0.1.0", + "@homarr/image-proxy": "workspace:^0.1.0", "@homarr/integrations": "workspace:^0.1.0", "@homarr/log": "workspace:^", "@homarr/modals": "workspace:^0.1.0", @@ -56,7 +57,7 @@ "@mantine/modals": "^8.1.3", "@mantine/tiptap": "^8.1.3", "@million/lint": "1.0.14", - "@tabler/icons-react": "^3.34.0", + "@tabler/icons-react": "^3.34.1", "@tanstack/react-query": "^5.83.0", "@tanstack/react-query-devtools": "^5.83.0", "@tanstack/react-query-next-experimental": "^5.83.0", @@ -75,7 +76,7 @@ "glob": "^11.0.3", "jotai": "^2.12.5", "mantine-react-table": "2.0.0-beta.9", - "next": "15.4.1", + "next": "15.4.2", "postcss-preset-mantine": "^1.18.0", "prismjs": "^1.30.0", "react": "19.1.0", diff --git a/apps/nextjs/src/app/api/image-proxy/[id]/route.ts b/apps/nextjs/src/app/api/image-proxy/[id]/route.ts new file mode 100644 index 000000000..26eb70dd8 --- /dev/null +++ b/apps/nextjs/src/app/api/image-proxy/[id]/route.ts @@ -0,0 +1,19 @@ +import { notFound } from "next/navigation"; + +import { ImageProxy } from "@homarr/image-proxy"; + +export const GET = async (_request: Request, props: { params: Promise<{ id: string }> }) => { + const { id } = await props.params; + + const imageProxy = new ImageProxy(); + const image = await imageProxy.forwardImageAsync(id); + if (!image) { + notFound(); + } + + return new Response(image, { + headers: { + "Cache-Control": "public, max-age=3600, immutable", // Cache for 1 hour + }, + }); +}; diff --git a/apps/nextjs/src/env.ts b/apps/nextjs/src/env.ts index 8e62d4dc2..62b49dee8 100644 --- a/apps/nextjs/src/env.ts +++ b/apps/nextjs/src/env.ts @@ -1,5 +1,4 @@ -import { createEnv } from "@homarr/env"; -import { createBooleanSchema } from "@homarr/env/schemas"; +import { createBooleanSchema, createEnv } from "@homarr/core/infrastructure/env"; export const env = createEnv({ server: { diff --git a/apps/tasks/package.json b/apps/tasks/package.json index b7f236b2e..cbd6c21d0 100644 --- a/apps/tasks/package.json +++ b/apps/tasks/package.json @@ -10,7 +10,7 @@ "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 --outfile=tasks.cjs", + "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", @@ -46,7 +46,7 @@ "@homarr/tsconfig": "workspace:^0.1.0", "@types/node": "^22.16.4", "dotenv-cli": "^8.0.0", - "esbuild": "^0.25.6", + "esbuild": "^0.25.8", "eslint": "^9.31.0", "prettier": "^3.6.2", "tsx": "4.20.3", diff --git a/apps/websocket/package.json b/apps/websocket/package.json index 48aab2e93..c53718f53 100644 --- a/apps/websocket/package.json +++ b/apps/websocket/package.json @@ -34,7 +34,7 @@ "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0", "@types/ws": "^8.18.1", - "esbuild": "^0.25.6", + "esbuild": "^0.25.8", "eslint": "^9.31.0", "prettier": "^3.6.2", "typescript": "^5.8.3" diff --git a/e2e/health-checks.spec.ts b/e2e/health-checks.spec.ts index 24fd26c91..03d175525 100644 --- a/e2e/health-checks.spec.ts +++ b/e2e/health-checks.spec.ts @@ -1,9 +1,10 @@ import { describe, expect, test } from "vitest"; import { createHomarrContainer } from "./shared/create-homarr-container"; +import { createRedisContainer } from "./shared/redis-container"; describe("Health checks", () => { - test("ready and live should return 200 OK", async () => { + test("ready and live should return 200 OK with normal image and no extra configuration", async () => { // Arrange const homarrContainer = await createHomarrContainer().start(); @@ -15,4 +16,31 @@ describe("Health checks", () => { expect(readyResponse.status).toBe(200); expect(liveResponse.status).toBe(200); }, 20_000); + + test("ready and live should return 200 OK with external redis", async () => { + // Arrange + const redisContainer = await createRedisContainer().start(); + const homarrContainer = await createHomarrContainer({ + environment: { + REDIS_IS_EXTERNAL: "true", + REDIS_HOST: "host.docker.internal", + REDIS_PORT: redisContainer.getMappedPort(6379).toString(), + REDIS_PASSWORD: redisContainer.getPassword(), + }, + }).start(); + + // Act + const readyResponse = await fetch(`http://localhost:${homarrContainer.getMappedPort(7575)}/api/health/ready`); + const liveResponse = await fetch(`http://localhost:${homarrContainer.getMappedPort(7575)}/api/health/live`); + + // Assert + expect( + readyResponse.status, + `Expected ready to return OK statusCode=${readyResponse.status} content=${await readyResponse.text()}`, + ).toBe(200); + expect( + liveResponse.status, + `Expected live to return OK statusCode=${liveResponse.status} content=${await liveResponse.text()}`, + ).toBe(200); + }, 20_000); }); diff --git a/e2e/shared/redis-container.ts b/e2e/shared/redis-container.ts new file mode 100644 index 000000000..cfd8ad4fb --- /dev/null +++ b/e2e/shared/redis-container.ts @@ -0,0 +1,5 @@ +import { RedisContainer } from "@testcontainers/redis"; + +export const createRedisContainer = () => { + return new RedisContainer("redis:latest").withPassword("homarr"); +}; diff --git a/package.json b/package.json index 05013280c..0f659878e 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "@semantic-release/github": "^11.0.3", "@semantic-release/npm": "^12.0.2", "@semantic-release/release-notes-generator": "^14.0.3", + "@testcontainers/redis": "^11.3.1", "@turbo/gen": "^2.5.5", "@vitejs/plugin-react": "^4.7.0", "@vitest/coverage-v8": "^3.2.4", @@ -47,7 +48,7 @@ "jsdom": "^26.1.0", "prettier": "^3.6.2", "semantic-release": "^24.2.7", - "testcontainers": "^11.2.1", + "testcontainers": "^11.3.1", "turbo": "^2.5.5", "typescript": "^5.8.3", "vite-tsconfig-paths": "^5.1.4", diff --git a/packages/api/package.json b/packages/api/package.json index 3a3d5fd75..c7f7c4f00 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -46,7 +46,7 @@ "@trpc/server": "^11.4.3", "@trpc/tanstack-react-query": "^11.4.3", "lodash.clonedeep": "^4.5.0", - "next": "15.4.1", + "next": "15.4.2", "react": "19.1.0", "react-dom": "19.1.0", "superjson": "2.2.2", diff --git a/packages/api/src/router/widgets/index.ts b/packages/api/src/router/widgets/index.ts index 77a1bd2ef..2ad920b5d 100644 --- a/packages/api/src/router/widgets/index.ts +++ b/packages/api/src/router/widgets/index.ts @@ -5,6 +5,7 @@ import { dnsHoleRouter } from "./dns-hole"; import { downloadsRouter } from "./downloads"; import { healthMonitoringRouter } from "./health-monitoring"; import { indexerManagerRouter } from "./indexer-manager"; +import { mediaReleaseRouter } from "./media-release"; import { mediaRequestsRouter } from "./media-requests"; import { mediaServerRouter } from "./media-server"; import { mediaTranscodingRouter } from "./media-transcoding"; @@ -27,6 +28,7 @@ export const widgetRouter = createTRPCRouter({ smartHome: smartHomeRouter, stockPrice: stockPriceRouter, mediaServer: mediaServerRouter, + mediaRelease: mediaReleaseRouter, calendar: calendarRouter, downloads: downloadsRouter, mediaRequests: mediaRequestsRouter, diff --git a/packages/api/src/router/widgets/media-release.ts b/packages/api/src/router/widgets/media-release.ts new file mode 100644 index 000000000..fddc174cc --- /dev/null +++ b/packages/api/src/router/widgets/media-release.ts @@ -0,0 +1,67 @@ +import { observable } from "@trpc/server/observable"; + +import type { Modify } from "@homarr/common/types"; +import type { Integration } from "@homarr/db/schema"; +import type { IntegrationKindByCategory } from "@homarr/definitions"; +import { getIntegrationKindsByCategory } from "@homarr/definitions"; +import type { MediaRelease } from "@homarr/integrations/types"; +import { mediaReleaseRequestHandler } from "@homarr/request-handler/media-release"; + +import { createManyIntegrationMiddleware } from "../../middlewares/integration"; +import { createTRPCRouter, publicProcedure } from "../../trpc"; + +export const mediaReleaseRouter = createTRPCRouter({ + getMediaReleases: publicProcedure + .concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("mediaRelease"))) + .query(async ({ ctx }) => { + const results = await Promise.all( + ctx.integrations.map(async (integration) => { + const innerHandler = mediaReleaseRequestHandler.handler(integration, {}); + const { data, timestamp } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false }); + + return { + integration: { + id: integration.id, + name: integration.name, + kind: integration.kind, + updatedAt: timestamp, + }, + releases: data, + }; + }), + ); + return results.flatMap((result) => + result.releases.map((release) => ({ + ...release, + integration: result.integration, + })), + ); + }), + + subscribeToReleases: publicProcedure + .concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("mediaRelease"))) + .subscription(({ ctx }) => { + return observable<{ + integration: Modify }>; + releases: MediaRelease[]; + }>((emit) => { + const unsubscribes: (() => void)[] = []; + for (const integrationWithSecrets of ctx.integrations) { + const { decryptedSecrets: _, ...integration } = integrationWithSecrets; + const innerHandler = mediaReleaseRequestHandler.handler(integrationWithSecrets, {}); + const unsubscribe = innerHandler.subscribe((releases) => { + emit.next({ + integration, + releases, + }); + }); + unsubscribes.push(unsubscribe); + } + return () => { + unsubscribes.forEach((unsubscribe) => { + unsubscribe(); + }); + }; + }); + }), +}); diff --git a/packages/auth/env.ts b/packages/auth/env.ts index 40878312f..12089d9c8 100644 --- a/packages/auth/env.ts +++ b/packages/auth/env.ts @@ -1,8 +1,7 @@ import { z } from "zod"; +import { createBooleanSchema, createDurationSchema, createEnv } from "@homarr/core/infrastructure/env"; import { supportedAuthProviders } from "@homarr/definitions"; -import { createEnv } from "@homarr/env"; -import { createBooleanSchema, createDurationSchema } from "@homarr/env/schemas"; const authProvidersSchema = z .string() diff --git a/packages/auth/package.json b/packages/auth/package.json index e99b57c3d..cb6bea4c0 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -27,15 +27,15 @@ "@auth/drizzle-adapter": "^1.10.0", "@homarr/certificates": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0", + "@homarr/core": "workspace:^0.1.0", "@homarr/db": "workspace:^0.1.0", "@homarr/definitions": "workspace:^0.1.0", - "@homarr/env": "workspace:^0.1.0", "@homarr/log": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0", "bcrypt": "^6.0.0", "cookies": "^0.9.1", "ldapts": "8.0.6", - "next": "15.4.1", + "next": "15.4.2", "next-auth": "5.0.0-beta.29", "react": "19.1.0", "react-dom": "19.1.0", diff --git a/packages/cli/package.json b/packages/cli/package.json index caabe59be..72e1c5e32 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -34,7 +34,7 @@ "@homarr/eslint-config": "workspace:^0.2.0", "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0", - "esbuild": "^0.25.6", + "esbuild": "^0.25.8", "eslint": "^9.31.0", "typescript": "^5.8.3" } diff --git a/packages/common/env.ts b/packages/common/env.ts index 9149bfc46..f9e734e0a 100644 --- a/packages/common/env.ts +++ b/packages/common/env.ts @@ -1,7 +1,7 @@ import { randomBytes } from "crypto"; import { z } from "zod"; -import { createEnv } from "@homarr/env"; +import { createEnv } from "@homarr/core/infrastructure/env"; const errorSuffix = `, please generate a 64 character secret in hex format or use the following: "${randomBytes(32).toString("hex")}"`; diff --git a/packages/common/package.json b/packages/common/package.json index c3f7c0551..b9e745dc5 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -27,11 +27,11 @@ }, "prettier": "@homarr/prettier-config", "dependencies": { - "@homarr/env": "workspace:^0.1.0", + "@homarr/core": "workspace:^0.1.0", "@homarr/log": "workspace:^0.1.0", "@paralleldrive/cuid2": "^2.2.2", "dayjs": "^1.11.13", - "next": "15.4.1", + "next": "15.4.2", "react": "19.1.0", "react-dom": "19.1.0", "undici": "7.12.0", diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index 7ffe71664..f7d667155 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -12,3 +12,4 @@ export * from "./error"; export * from "./fetch-with-timeout"; export * from "./theme"; export * from "./function"; +export * from "./id"; diff --git a/packages/env/eslint.config.js b/packages/core/eslint.config.js similarity index 62% rename from packages/env/eslint.config.js rename to packages/core/eslint.config.js index 5b19b6f8a..f7a5a7d36 100644 --- a/packages/env/eslint.config.js +++ b/packages/core/eslint.config.js @@ -1,9 +1,4 @@ import baseConfig from "@homarr/eslint-config/base"; /** @type {import('typescript-eslint').Config} */ -export default [ - { - ignores: [], - }, - ...baseConfig, -]; +export default [...baseConfig]; diff --git a/packages/env/package.json b/packages/core/package.json similarity index 77% rename from packages/env/package.json rename to packages/core/package.json index 8b2cd50db..92183a1a1 100644 --- a/packages/env/package.json +++ b/packages/core/package.json @@ -1,12 +1,13 @@ { - "name": "@homarr/env", + "name": "@homarr/core", "version": "0.1.0", "private": true, "license": "Apache-2.0", "type": "module", "exports": { - ".": "./index.ts", - "./schemas": "./src/schemas.ts" + "./infrastructure/redis": "./src/infrastructure/redis/client.ts", + "./infrastructure/env": "./src/infrastructure/env/index.ts", + ".": "./src/index.ts" }, "typesVersions": { "*": { @@ -24,6 +25,7 @@ "prettier": "@homarr/prettier-config", "dependencies": { "@t3-oss/env-nextjs": "^0.13.8", + "ioredis": "5.6.1", "zod": "^3.25.76" }, "devDependencies": { diff --git a/packages/env/src/index.ts b/packages/core/src/infrastructure/env/index.ts similarity index 88% rename from packages/env/src/index.ts rename to packages/core/src/infrastructure/env/index.ts index e77d8f61a..e11f3323d 100644 --- a/packages/env/src/index.ts +++ b/packages/core/src/infrastructure/env/index.ts @@ -7,3 +7,6 @@ export const defaultEnvOptions = { } satisfies Partial[0]>; export const createEnv: typeof createEnvT3 = (options) => createEnvT3({ ...defaultEnvOptions, ...options }); + +export * from "./prefix"; +export * from "./schemas"; diff --git a/packages/core/src/infrastructure/env/prefix.ts b/packages/core/src/infrastructure/env/prefix.ts new file mode 100644 index 000000000..a113cd9e1 --- /dev/null +++ b/packages/core/src/infrastructure/env/prefix.ts @@ -0,0 +1,13 @@ +export const runtimeEnvWithPrefix = (prefix: `${string}_`) => + Object.entries(process.env) + .filter(([key]) => key.startsWith(prefix)) + .reduce( + (acc, [key, value]) => { + if (value === undefined) return acc; + + const newKey = key.replace(prefix, ""); + acc[newKey] = value; + return acc; + }, + {} as Record, + ); diff --git a/packages/env/src/schemas.ts b/packages/core/src/infrastructure/env/schemas.ts similarity index 100% rename from packages/env/src/schemas.ts rename to packages/core/src/infrastructure/env/schemas.ts diff --git a/packages/core/src/infrastructure/redis/client.ts b/packages/core/src/infrastructure/redis/client.ts new file mode 100644 index 000000000..e35ee9220 --- /dev/null +++ b/packages/core/src/infrastructure/redis/client.ts @@ -0,0 +1,26 @@ +import type { RedisOptions } from "ioredis"; +import { Redis } from "ioredis"; + +import { redisEnv } from "./env"; + +const defaultRedisOptions = { + connectionName: "homarr", +} satisfies RedisOptions; + +export type { Redis as RedisClient } from "ioredis"; + +export const createRedisClient = () => + redisEnv.IS_EXTERNAL + ? new Redis({ + ...defaultRedisOptions, + host: redisEnv.HOST, + port: redisEnv.PORT, + tls: redisEnv.TLS_CA + ? { + ca: redisEnv.TLS_CA, + } + : undefined, + username: redisEnv.USERNAME, + password: redisEnv.PASSWORD, + }) + : new Redis(defaultRedisOptions); diff --git a/packages/core/src/infrastructure/redis/env.ts b/packages/core/src/infrastructure/redis/env.ts new file mode 100644 index 000000000..f54ec128c --- /dev/null +++ b/packages/core/src/infrastructure/redis/env.ts @@ -0,0 +1,17 @@ +import { z } from "zod/v4"; + +import { createEnv } from "../env"; +import { runtimeEnvWithPrefix } from "../env/prefix"; +import { createBooleanSchema } from "../env/schemas"; + +export const redisEnv = createEnv({ + server: { + IS_EXTERNAL: createBooleanSchema(false), + HOST: z.string().optional(), + PORT: z.coerce.number().default(6379).optional(), + TLS_CA: z.string().optional(), + USERNAME: z.string().optional(), + PASSWORD: z.string().optional(), + }, + runtimeEnv: runtimeEnvWithPrefix("REDIS_"), +}); diff --git a/packages/env/tsconfig.json b/packages/core/tsconfig.json similarity index 100% rename from packages/env/tsconfig.json rename to packages/core/tsconfig.json diff --git a/packages/cron-job-api/package.json b/packages/cron-job-api/package.json index 828a5297f..88e248fbb 100644 --- a/packages/cron-job-api/package.json +++ b/packages/cron-job-api/package.json @@ -26,8 +26,8 @@ "prettier": "@homarr/prettier-config", "dependencies": { "@homarr/common": "workspace:^0.1.0", + "@homarr/core": "workspace:^0.1.0", "@homarr/cron-jobs": "workspace:^0.1.0", - "@homarr/env": "workspace:^0.1.0", "@homarr/log": "workspace:^0.1.0", "@tanstack/react-query": "^5.83.0", "@trpc/client": "^11.4.3", diff --git a/packages/cron-job-api/src/env.ts b/packages/cron-job-api/src/env.ts index 1bc8f644a..7efb3f6e2 100644 --- a/packages/cron-job-api/src/env.ts +++ b/packages/cron-job-api/src/env.ts @@ -1,7 +1,7 @@ import { z } from "zod/v4"; import { env as commonEnv } from "@homarr/common/env"; -import { createEnv } from "@homarr/env"; +import { createEnv } from "@homarr/core/infrastructure/env"; export const env = createEnv({ server: { diff --git a/packages/db/env.ts b/packages/db/env.ts index dd8590b26..bb7ad0388 100644 --- a/packages/db/env.ts +++ b/packages/db/env.ts @@ -1,7 +1,7 @@ import { z } from "zod"; import { env as commonEnv } from "@homarr/common/env"; -import { createEnv } from "@homarr/env"; +import { createEnv } from "@homarr/core/infrastructure/env"; const drivers = { betterSqlite3: "better-sqlite3", diff --git a/packages/db/package.json b/packages/db/package.json index 0c2dd493b..bca8c851d 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -40,13 +40,13 @@ "dependencies": { "@auth/core": "^0.40.0", "@homarr/common": "workspace:^0.1.0", + "@homarr/core": "workspace:^0.1.0", "@homarr/definitions": "workspace:^0.1.0", - "@homarr/env": "workspace:^0.1.0", "@homarr/log": "workspace:^0.1.0", "@homarr/server-settings": "workspace:^0.1.0", "@mantine/core": "^8.1.3", "@paralleldrive/cuid2": "^2.2.2", - "@testcontainers/mysql": "^11.2.1", + "@testcontainers/mysql": "^11.3.1", "better-sqlite3": "^12.2.0", "dotenv": "^17.2.0", "drizzle-kit": "^0.31.4", @@ -61,7 +61,7 @@ "@homarr/tsconfig": "workspace:^0.1.0", "@types/better-sqlite3": "7.6.13", "dotenv-cli": "^8.0.0", - "esbuild": "^0.25.6", + "esbuild": "^0.25.8", "eslint": "^9.31.0", "prettier": "^3.6.2", "tsx": "4.20.3", diff --git a/packages/definitions/src/integration.ts b/packages/definitions/src/integration.ts index 1506182f1..7a1112aba 100644 --- a/packages/definitions/src/integration.ts +++ b/packages/definitions/src/integration.ts @@ -92,19 +92,19 @@ export const integrationDefs = { name: "Jellyfin", secretKinds: [["username", "password"], ["apiKey"]], iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/jellyfin.svg", - category: ["mediaService"], + category: ["mediaService", "mediaRelease"], }, emby: { name: "Emby", secretKinds: [["apiKey"]], iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/emby.svg", - category: ["mediaService"], + category: ["mediaService", "mediaRelease"], }, plex: { name: "Plex", secretKinds: [["apiKey"]], iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/plex.svg", - category: ["mediaService"], + category: ["mediaService", "mediaRelease"], }, jellyseerr: { name: "Jellyseerr", @@ -224,6 +224,7 @@ export const integrationDefs = { "downloadClient", "healthMonitoring", "indexerManager", + "mediaRelease", "mediaRequest", "mediaService", "mediaTranscoding", @@ -282,6 +283,7 @@ export const integrationCategories = [ "mediaService", "calendar", "mediaSearch", + "mediaRelease", "mediaRequest", "downloadClient", "usenet", diff --git a/packages/definitions/src/widget.ts b/packages/definitions/src/widget.ts index 609e8bf9a..24da8f214 100644 --- a/packages/definitions/src/widget.ts +++ b/packages/definitions/src/widget.ts @@ -24,6 +24,7 @@ export const widgetKinds = [ "indexerManager", "healthMonitoring", "releases", + "mediaReleases", "dockerContainers", "notifications", ] as const; diff --git a/packages/docker/package.json b/packages/docker/package.json index 2de072d30..534211153 100644 --- a/packages/docker/package.json +++ b/packages/docker/package.json @@ -25,7 +25,7 @@ "prettier": "@homarr/prettier-config", "dependencies": { "@homarr/common": "workspace:^0.1.0", - "@homarr/env": "workspace:^0.1.0", + "@homarr/core": "workspace:^0.1.0", "dockerode": "^4.0.7" }, "devDependencies": { diff --git a/packages/docker/src/env.ts b/packages/docker/src/env.ts index e71a26a66..a0ba6c5e2 100644 --- a/packages/docker/src/env.ts +++ b/packages/docker/src/env.ts @@ -1,7 +1,6 @@ import { z } from "zod"; -import { createEnv } from "@homarr/env"; -import { createBooleanSchema } from "@homarr/env/schemas"; +import { createBooleanSchema, createEnv } from "@homarr/core/infrastructure/env"; export const env = createEnv({ server: { diff --git a/packages/env/index.ts b/packages/env/index.ts deleted file mode 100644 index 3bd16e178..000000000 --- a/packages/env/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./src"; diff --git a/packages/image-proxy/eslint.config.js b/packages/image-proxy/eslint.config.js new file mode 100644 index 000000000..f7a5a7d36 --- /dev/null +++ b/packages/image-proxy/eslint.config.js @@ -0,0 +1,4 @@ +import baseConfig from "@homarr/eslint-config/base"; + +/** @type {import('typescript-eslint').Config} */ +export default [...baseConfig]; diff --git a/packages/image-proxy/package.json b/packages/image-proxy/package.json new file mode 100644 index 000000000..638d21704 --- /dev/null +++ b/packages/image-proxy/package.json @@ -0,0 +1,39 @@ +{ + "name": "@homarr/image-proxy", + "version": "0.1.0", + "private": true, + "license": "Apache-2.0", + "type": "module", + "exports": { + ".": "./src/index.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/certificates": "workspace:^0.1.0", + "@homarr/common": "workspace:^0.1.0", + "@homarr/log": "workspace:^0.1.0", + "@homarr/redis": "workspace:^0.1.0", + "bcrypt": "^6.0.0" + }, + "devDependencies": { + "@homarr/eslint-config": "workspace:^0.2.0", + "@homarr/prettier-config": "workspace:^0.1.0", + "@homarr/tsconfig": "workspace:^0.1.0", + "@types/bcrypt": "5.0.2", + "eslint": "^9.31.0", + "typescript": "^5.8.3" + } +} diff --git a/packages/image-proxy/src/index.ts b/packages/image-proxy/src/index.ts new file mode 100644 index 000000000..19f66e608 --- /dev/null +++ b/packages/image-proxy/src/index.ts @@ -0,0 +1,133 @@ +import bcrypt from "bcrypt"; + +import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; +import { createId } from "@homarr/common"; +import { decryptSecret, encryptSecret } from "@homarr/common/server"; +import { logger } from "@homarr/log"; +import { createGetSetChannel } from "@homarr/redis"; + +const createHashChannel = (hash: `${string}.${string}`) => createGetSetChannel(`image-proxy:hash:${hash}`); +const createUrlByIdChannel = (id: string) => + createGetSetChannel<{ + url: `${string}.${string}`; + headers: `${string}.${string}`; + }>(`image-proxy:url:${id}`); +const saltChannel = createGetSetChannel("image-proxy:salt"); + +export class ImageProxy { + private static salt: string | null = null; + private async getOrCreateSaltAsync(): Promise { + if (ImageProxy.salt) return ImageProxy.salt; + const existingSalt = await saltChannel.getAsync(); + if (existingSalt) { + ImageProxy.salt = existingSalt; + return existingSalt; + } + + const salt = await bcrypt.genSalt(10); + logger.debug(`Generated new salt for image proxy salt="${salt}"`); + ImageProxy.salt = salt; + await saltChannel.setAsync(salt); + return salt; + } + + public async createImageAsync(url: string, headers?: Record): Promise { + const existingId = await this.getExistingIdAsync(url, headers); + if (existingId) { + logger.debug( + `Image already exists in the proxy id="${existingId}" url="${this.redactUrl(url)}" headers="${this.redactHeaders(headers ?? null)}"`, + ); + return this.createImageUrl(existingId); + } + + const id = createId(); + await this.storeImageAsync(id, url, headers); + + return this.createImageUrl(id); + } + + public async forwardImageAsync(id: string): Promise { + const urlAndHeaders = await this.getImageUrlAndHeadersAsync(id); + if (!urlAndHeaders) { + return null; + } + + const response = await fetchWithTrustedCertificatesAsync(urlAndHeaders.url, { + headers: urlAndHeaders.headers ?? {}, + }); + + const proxyUrl = this.createImageUrl(id); + if (!response.ok) { + logger.error( + `Failed to fetch image id="${id}" url="${this.redactUrl(urlAndHeaders.url)}" headers="${this.redactHeaders(urlAndHeaders.headers)}" proxyUrl="${proxyUrl}" statusCode="${response.status}"`, + ); + return null; + } + + const blob = (await response.blob()) as Blob; + logger.debug( + `Forwarding image succeeded id="${id}" url="${this.redactUrl(urlAndHeaders.url)}" headers="${this.redactHeaders(urlAndHeaders.headers)}" proxyUrl="${proxyUrl} size="${(blob.size / 1024).toFixed(1)}KB"`, + ); + + return blob; + } + + private createImageUrl(id: string): string { + return `/api/image-proxy/${id}`; + } + + private async getImageUrlAndHeadersAsync(id: string) { + const urlHeaderChannel = createUrlByIdChannel(id); + const urlHeader = await urlHeaderChannel.getAsync(); + if (!urlHeader) { + logger.warn(`Image not found in the proxy id="${id}"`); + return null; + } + + return { + url: decryptSecret(urlHeader.url), + headers: JSON.parse(decryptSecret(urlHeader.headers)) as Record | null, + }; + } + + private async getExistingIdAsync(url: string, headers: Record | undefined): Promise { + const salt = await this.getOrCreateSaltAsync(); + const urlHash = await bcrypt.hash(url, salt); + const headerHash = await bcrypt.hash(JSON.stringify(headers ?? null), salt); + + const channel = createHashChannel(`${urlHash}.${headerHash}`); + return await channel.getAsync(); + } + + private async storeImageAsync(id: string, url: string, headers: Record | undefined): Promise { + const salt = await this.getOrCreateSaltAsync(); + const urlHash = await bcrypt.hash(url, salt); + const headerHash = await bcrypt.hash(JSON.stringify(headers ?? null), salt); + + const hashChannel = createHashChannel(`${urlHash}.${headerHash}`); + const urlHeaderChannel = createUrlByIdChannel(id); + await urlHeaderChannel.setAsync({ + url: encryptSecret(url), + headers: encryptSecret(JSON.stringify(headers ?? null)), + }); + await hashChannel.setAsync(id); + + logger.debug( + `Stored image in the proxy id="${id}" url="${this.redactUrl(url)}" headers="${this.redactHeaders(headers ?? null)}"`, + ); + } + + private redactUrl(url: string): string { + const urlObject = new URL(url); + + const redactedSearch = [...urlObject.searchParams.keys()].map((key) => `${key}=REDACTED`).join("&"); + + return `${urlObject.origin}${urlObject.pathname}${redactedSearch ? `?${redactedSearch}` : ""}`; + } + + private redactHeaders(headers: Record | null): string | null { + if (!headers) return null; + + return Object.keys(headers).join(", "); + } +} diff --git a/packages/image-proxy/tsconfig.json b/packages/image-proxy/tsconfig.json new file mode 100644 index 000000000..612bef8df --- /dev/null +++ b/packages/image-proxy/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "@homarr/tsconfig/base.json", + "compilerOptions": { + "types": ["node"], + "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json" + }, + "include": ["*.ts", "src"], + "exclude": ["node_modules"] +} diff --git a/packages/integrations/package.json b/packages/integrations/package.json index da43daf54..c4c4597b8 100644 --- a/packages/integrations/package.json +++ b/packages/integrations/package.json @@ -28,11 +28,12 @@ "@ctrl/deluge": "^7.1.0", "@ctrl/qbittorrent": "^9.6.0", "@ctrl/transmission": "^7.2.0", - "@gitbeaker/rest": "^42.5.0", + "@gitbeaker/rest": "^43.3.0", "@homarr/certificates": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0", "@homarr/db": "workspace:^0.1.0", "@homarr/definitions": "workspace:^0.1.0", + "@homarr/image-proxy": "workspace:^0.1.0", "@homarr/log": "workspace:^0.1.0", "@homarr/node-unifi": "^2.6.0", "@homarr/redis": "workspace:^0.1.0", diff --git a/packages/integrations/src/emby/emby-integration.ts b/packages/integrations/src/emby/emby-integration.ts index e05ba96f5..2b354cbe1 100644 --- a/packages/integrations/src/emby/emby-integration.ts +++ b/packages/integrations/src/emby/emby-integration.ts @@ -2,6 +2,7 @@ import { BaseItemKind } from "@jellyfin/sdk/lib/generated-client/models"; import { z } from "zod"; import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; +import { ResponseError } from "@homarr/common/server"; import type { IntegrationTestingInput } from "../base/integration"; import { Integration } from "../base/integration"; @@ -10,6 +11,7 @@ import type { TestingResult } from "../base/test-connection/test-connection-serv import type { IMediaServerIntegration } from "../interfaces/media-server/media-server-integration"; import type { CurrentSessionsInput, StreamSession } from "../interfaces/media-server/media-server-types"; import { convertJellyfinType } from "../jellyfin/jellyfin-integration"; +import type { IMediaReleasesIntegration, MediaRelease } from "../types"; const sessionSchema = z.object({ NowPlayingItem: z @@ -31,7 +33,34 @@ const sessionSchema = z.object({ UserName: z.string().nullish(), }); -export class EmbyIntegration extends Integration implements IMediaServerIntegration { +const itemSchema = z.object({ + Id: z.string(), + ServerId: z.string(), + Name: z.string(), + Taglines: z.array(z.string()), + Studios: z.array(z.object({ Name: z.string() })), + Overview: z.string().optional(), + PremiereDate: z + .string() + .datetime() + .transform((date) => new Date(date)) + .optional(), + DateCreated: z + .string() + .datetime() + .transform((date) => new Date(date)), + Genres: z.array(z.string()), + CommunityRating: z.number().optional(), + RunTimeTicks: z.number(), + Type: z.string(), // for example "Movie" +}); + +const userSchema = z.object({ + Id: z.string(), + Name: z.string(), +}); + +export class EmbyIntegration extends Integration implements IMediaServerIntegration, IMediaReleasesIntegration { private static readonly apiKeyHeader = "X-Emby-Token"; private static readonly deviceId = "homarr-emby-integration"; private static readonly authorizationHeaderValue = `Emby Client="Dashboard", Device="Homarr", DeviceId="${EmbyIntegration.deviceId}", Version="0.0.1"`; @@ -103,4 +132,69 @@ export class EmbyIntegration extends Integration implements IMediaServerIntegrat }; }); } + + public async getMediaReleasesAsync(): Promise { + const limit = 100; + const users = await this.fetchUsersPublicAsync(); + const userId = users.at(0)?.id; + if (!userId) { + throw new Error("No users found"); + } + + const apiKey = super.getSecretValue("apiKey"); + const response = await fetchWithTrustedCertificatesAsync( + super.url( + `/Users/${userId}/Items/Latest?Limit=${limit}&Fields=CommunityRating,Studios,PremiereDate,Genres,ChildCount,ProductionYear,DateCreated,Overview,Taglines`, + ), + { + headers: { + [EmbyIntegration.apiKeyHeader]: apiKey, + Authorization: EmbyIntegration.authorizationHeaderValue, + }, + }, + ); + + if (!response.ok) { + throw new ResponseError(response); + } + + const items = z.array(itemSchema).parse(await response.json()); + + return items.map((item) => ({ + id: item.Id, + type: item.Type === "Movie" ? "movie" : item.Type === "Series" ? "tv" : "unknown", + title: item.Name, + subtitle: item.Taglines.at(0), + description: item.Overview, + releaseDate: item.PremiereDate ?? item.DateCreated, + imageUrls: { + poster: super.url(`/Items/${item.Id}/Images/Primary?maxHeight=492&maxWidth=328&quality=90`).toString(), + backdrop: super.url(`/Items/${item.Id}/Images/Backdrop/0?maxWidth=960&quality=70`).toString(), + }, + producer: item.Studios.at(0)?.Name, + rating: item.CommunityRating?.toFixed(1), + tags: item.Genres, + href: super.url(`/web/index.html#!/item?id=${item.Id}&serverId=${item.ServerId}`).toString(), + })); + } + + // https://dev.emby.media/reference/RestAPI/UserService/getUsersPublic.html + private async fetchUsersPublicAsync(): Promise<{ id: string; name: string }[]> { + const apiKey = super.getSecretValue("apiKey"); + const response = await fetchWithTrustedCertificatesAsync(super.url("/Users/Public"), { + headers: { + [EmbyIntegration.apiKeyHeader]: apiKey, + Authorization: EmbyIntegration.authorizationHeaderValue, + }, + }); + if (!response.ok) { + throw new ResponseError(response); + } + const users = z.array(userSchema).parse(await response.json()); + + return users.map((user) => ({ + id: user.Id, + name: user.Name, + })); + } } diff --git a/packages/integrations/src/interfaces/media-releases.ts b/packages/integrations/src/interfaces/media-releases.ts new file mode 100644 index 000000000..face87f72 --- /dev/null +++ b/packages/integrations/src/interfaces/media-releases.ts @@ -0,0 +1,76 @@ +import type { MantineColor } from "@mantine/core"; + +export const mediaTypeConfigurations = { + movie: { + color: "blue", + }, + tv: { + color: "violet", + }, + music: { + color: "green", + }, + book: { + color: "orange", + }, + game: { + color: "yellow", + }, + video: { + color: "red", + }, + article: { + color: "pink", + }, + unknown: { + color: "gray", + }, +} satisfies Record; + +export type MediaType = keyof typeof mediaTypeConfigurations; + +export interface MediaRelease { + id: string; + type: MediaType; + title: string; + /** + * The subtitle of the media item, if applicable. + * Can also contain the season number for TV shows. + */ + subtitle?: string; + description?: string; + releaseDate: Date; + imageUrls: { + poster: string | undefined; + backdrop: string | undefined; + }; + /** + * The name of the studio, publisher or author. + */ + producer?: string; + /** + * Price in USD + */ + price?: number; + /** + * Rating in any format (e.g. 5/10, 4.5/5, 90%, etc.) + */ + rating?: string; + /** + * List of tags / genres / categories + */ + tags: string[]; + /** + * Link to the media item + */ + href: string; + /* + * Video / Music: duration in seconds + * Book: number of pages + */ + length?: number; +} + +export interface IMediaReleasesIntegration { + getMediaReleasesAsync(): Promise; +} diff --git a/packages/integrations/src/jellyfin/jellyfin-integration.ts b/packages/integrations/src/jellyfin/jellyfin-integration.ts index 5bfcb2e3f..0a31af85b 100644 --- a/packages/integrations/src/jellyfin/jellyfin-integration.ts +++ b/packages/integrations/src/jellyfin/jellyfin-integration.ts @@ -2,6 +2,8 @@ import { Jellyfin } from "@jellyfin/sdk"; import { BaseItemKind } from "@jellyfin/sdk/lib/generated-client/models"; import { getSessionApi } from "@jellyfin/sdk/lib/utils/api/session-api"; import { getSystemApi } from "@jellyfin/sdk/lib/utils/api/system-api"; +import { getUserApi } from "@jellyfin/sdk/lib/utils/api/user-api"; +import { getUserLibraryApi } from "@jellyfin/sdk/lib/utils/api/user-library-api"; import type { AxiosInstance } from "axios"; import { createAxiosCertificateInstanceAsync } from "@homarr/certificates/server"; @@ -13,9 +15,10 @@ import { Integration } from "../base/integration"; import type { TestingResult } from "../base/test-connection/test-connection-service"; import type { IMediaServerIntegration } from "../interfaces/media-server/media-server-integration"; import type { CurrentSessionsInput, StreamSession } from "../interfaces/media-server/media-server-types"; +import type { IMediaReleasesIntegration, MediaRelease } from "../types"; @HandleIntegrationErrors([integrationAxiosHttpErrorHandler]) -export class JellyfinIntegration extends Integration implements IMediaServerIntegration { +export class JellyfinIntegration extends Integration implements IMediaServerIntegration, IMediaReleasesIntegration { private readonly jellyfin: Jellyfin = new Jellyfin({ clientInfo: { name: "Homarr", @@ -70,6 +73,43 @@ export class JellyfinIntegration extends Integration implements IMediaServerInte }); } + public async getMediaReleasesAsync(): Promise { + const apiClient = await this.getApiAsync(); + const userLibraryApi = getUserLibraryApi(apiClient); + const userApi = getUserApi(apiClient); + + const users = await userApi.getUsers(); + const userId = users.data.at(0)?.Id; + if (!userId) { + throw new Error("No users found"); + } + + const result = await userLibraryApi.getLatestMedia({ + fields: ["CustomRating", "Studios", "Genres", "ChildCount", "DateCreated", "Overview", "Taglines"], + userId, + limit: 100, + }); + return result.data.map((item) => ({ + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + id: item.Id!, + type: item.Type === "Movie" ? "movie" : item.Type === "Series" ? "tv" : "unknown", + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + title: item.Name!, + subtitle: item.Taglines?.at(0), + description: item.Overview ?? undefined, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + releaseDate: new Date(item.PremiereDate ?? item.DateCreated!), + imageUrls: { + poster: super.url(`/Items/${item.Id}/Images/Primary?maxHeight=492&maxWidth=328&quality=90`).toString(), + backdrop: super.url(`/Items/${item.Id}/Images/Backdrop/0?maxWidth=960&quality=70`).toString(), + }, + producer: item.Studios?.at(0)?.Name ?? undefined, + rating: item.CommunityRating?.toFixed(1), + tags: item.Genres ?? [], + href: super.url(`/web/index.html#!/details?id=${item.Id}&serverId=${item.ServerId}`).toString(), + })); + } + /** * Constructs an ApiClient synchronously with an ApiKey or asynchronously * with a username and password. diff --git a/packages/integrations/src/mock/data/media-releases.ts b/packages/integrations/src/mock/data/media-releases.ts new file mode 100644 index 000000000..d820a9aa4 --- /dev/null +++ b/packages/integrations/src/mock/data/media-releases.ts @@ -0,0 +1,128 @@ +import type { IMediaReleasesIntegration, MediaRelease } from "../../interfaces/media-releases"; + +export class MediaReleasesMockService implements IMediaReleasesIntegration { + public async getMediaReleasesAsync(): Promise { + return await Promise.resolve(mockMediaReleases); + } +} + +export const mockMediaReleases: MediaRelease[] = [ + { + id: "1", + type: "movie", + title: "Inception", + subtitle: "A mind-bending thriller", + description: + "A thief who steals corporate secrets through the use of dream-sharing technology is given the inverse task of planting an idea into the mind of a CEO.", + releaseDate: new Date("2010-07-16"), + imageUrls: { + poster: "https://media.outnow.ch/Movies/Bilder/2025/MinecraftMovie/015.jpg", + backdrop: "https://example.com/inception_backdrop.jpg", + }, + producer: "Warner Bros.", + price: 14.99, + rating: "8.8/10", + tags: ["Sci-Fi", "Thriller"], + href: "https://example.com/inception", + length: 148, + }, + { + id: "2", + type: "tv", + title: "Breaking Bad", + subtitle: "S5E14 - Ozymandias", + description: "When Walter White's secret is revealed, he must face the consequences of his actions.", + releaseDate: new Date("2013-09-15"), + imageUrls: { + poster: "https://media.outnow.ch/Movies/Bilder/2025/MinecraftMovie/015.jpg", + backdrop: "https://example.com/breaking_bad_backdrop.jpg", + }, + producer: "AMC", + rating: "9.5/10", + tags: ["Crime", "Drama"], + href: "https://example.com/breaking_bad", + }, + { + id: "3", + type: "music", + title: "Random Access Memories", + subtitle: "Daft Punk", + description: "The fourth studio album by French electronic music duo Daft Punk.", + releaseDate: new Date("2013-05-17"), + imageUrls: { + poster: "https://media.outnow.ch/Movies/Bilder/2025/MinecraftMovie/015.jpg", + backdrop: "https://example.com/ram_backdrop.jpg", + }, + producer: "Columbia Records", + price: 9.99, + rating: "8.5/10", + tags: ["Electronic", "Dance", "Pop", "Funk"], + href: "https://example.com/ram", + }, + { + id: "4", + type: "book", + title: "The Great Gatsby", + subtitle: "F. Scott Fitzgerald", + description: "A novel about the American dream and the disillusionment that comes with it.", + releaseDate: new Date("1925-04-10"), + imageUrls: { + poster: "https://media.outnow.ch/Movies/Bilder/2025/MinecraftMovie/015.jpg", + backdrop: "https://example.com/gatsby_backdrop.jpg", + }, + producer: "Scribner", + price: 10.99, + rating: "4.2/5", + tags: ["Classic", "Fiction"], + href: "https://example.com/gatsby", + }, + { + id: "5", + type: "game", + title: "The Legend of Zelda: Breath of the Wild", + subtitle: "Nintendo Switch", + description: "An open-world action-adventure game set in the fantasy land of Hyrule.", + releaseDate: new Date("2017-03-03"), + imageUrls: { + poster: "https://media.outnow.ch/Movies/Bilder/2025/MinecraftMovie/015.jpg", + backdrop: "https://example.com/zelda_backdrop.jpg", + }, + producer: "Nintendo", + price: 59.99, + rating: "10/10", + tags: ["Action", "Adventure"], + href: "https://example.com/zelda", + }, + { + id: "6", + type: "article", + title: "The Rise of AI in Healthcare", + subtitle: "Tech Innovations", + description: "Exploring the impact of artificial intelligence on the healthcare industry.", + releaseDate: new Date("2023-10-01"), + imageUrls: { + poster: "https://media.outnow.ch/Movies/Bilder/2025/MinecraftMovie/015.jpg", + backdrop: "https://example.com/ai_healthcare_backdrop.jpg", + }, + producer: "Tech Innovations", + rating: "4.8/5", + tags: ["Technology", "Healthcare"], + href: "https://example.com/ai_healthcare", + }, + { + id: "7", + type: "video", + title: "Wir LIEBEN unsere MAMAS | 50 Fragen zu Mamas", + releaseDate: new Date("2024-05-18T17:00:00Z"), + imageUrls: { + poster: + "https://i.ytimg.com/vi/a3qyfXc1Pfg/hq720.jpg?sqp=-oaymwEnCNAFEJQDSFryq4qpAxkIARUAAIhCGAHYAQHiAQoIGBACGAY4AUAB&rs=AOn4CLBQKm0viRlfRjTV-V24vGO83rPaVw", + backdrop: + "https://i.ytimg.com/vi/a3qyfXc1Pfg/hq720.jpg?sqp=-oaymwEnCNAFEJQDSFryq4qpAxkIARUAAIhCGAHYAQHiAQoIGBACGAY4AUAB&rs=AOn4CLBQKm0viRlfRjTV-V24vGO83rPaVw", + }, + producer: "PietSmiet", + rating: "1K", + tags: [], + href: "https://www.youtube.com/watch?v=a3qyfXc1Pfg", + }, +]; diff --git a/packages/integrations/src/mock/mock-integration.ts b/packages/integrations/src/mock/mock-integration.ts index c3fc71c4d..f04959171 100644 --- a/packages/integrations/src/mock/mock-integration.ts +++ b/packages/integrations/src/mock/mock-integration.ts @@ -9,6 +9,7 @@ import type { ISystemHealthMonitoringIntegration, } from "../interfaces/health-monitoring/health-monitoring-integration"; import type { IIndexerManagerIntegration } from "../interfaces/indexer-manager/indexer-manager-integration"; +import type { IMediaReleasesIntegration } from "../interfaces/media-releases"; import type { IMediaRequestIntegration } from "../interfaces/media-requests/media-request-integration"; import type { IMediaServerIntegration } from "../interfaces/media-server/media-server-integration"; import type { IMediaTranscodingIntegration } from "../interfaces/media-transcoding/media-transcoding-integration"; @@ -19,6 +20,7 @@ import { ClusterHealthMonitoringMockService } from "./data/cluster-health-monito import { DnsHoleMockService } from "./data/dns-hole"; import { DownloadClientMockService } from "./data/download"; import { IndexerManagerMockService } from "./data/indexer-manager"; +import { MediaReleasesMockService } from "./data/media-releases"; import { MediaRequestMockService } from "./data/media-request"; import { MediaServerMockService } from "./data/media-server"; import { MediaTranscodingMockService } from "./data/media-transcoding"; @@ -36,6 +38,7 @@ export class MockIntegration IClusterHealthMonitoringIntegration, ISystemHealthMonitoringIntegration, IIndexerManagerIntegration, + IMediaReleasesIntegration, IMediaRequestIntegration, IMediaServerIntegration, IMediaTranscodingIntegration, @@ -48,6 +51,7 @@ export class MockIntegration private static readonly clusterMonitoring = new ClusterHealthMonitoringMockService(); private static readonly systemMonitoring = new SystemHealthMonitoringMockService(); private static readonly indexerManager = new IndexerManagerMockService(); + private static readonly mediaReleases = new MediaReleasesMockService(); private static readonly mediaRequest = new MediaRequestMockService(); private static readonly mediaServer = new MediaServerMockService(); private static readonly mediaTranscoding = new MediaTranscodingMockService(); @@ -87,6 +91,9 @@ export class MockIntegration getIndexersAsync = MockIntegration.indexerManager.getIndexersAsync.bind(MockIntegration.indexerManager); testAllAsync = MockIntegration.indexerManager.testAllAsync.bind(MockIntegration.indexerManager); + // MediaReleasesIntegration + getMediaReleasesAsync = MockIntegration.mediaReleases.getMediaReleasesAsync.bind(MockIntegration.mediaReleases); + // MediaRequestIntegration getSeriesInformationAsync = MockIntegration.mediaRequest.getSeriesInformationAsync.bind(MockIntegration.mediaRequest); requestMediaAsync = MockIntegration.mediaRequest.requestMediaAsync.bind(MockIntegration.mediaRequest); diff --git a/packages/integrations/src/plex/plex-integration.ts b/packages/integrations/src/plex/plex-integration.ts index 1025d9df2..c8d4582e8 100644 --- a/packages/integrations/src/plex/plex-integration.ts +++ b/packages/integrations/src/plex/plex-integration.ts @@ -1,7 +1,9 @@ import { parseStringPromise } from "xml2js"; +import { z } from "zod"; import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; import { ParseError } from "@homarr/common/server"; +import { ImageProxy } from "@homarr/image-proxy"; import { logger } from "@homarr/log"; import type { IntegrationTestingInput } from "../base/integration"; @@ -10,9 +12,10 @@ import { TestConnectionError } from "../base/test-connection/test-connection-err import type { TestingResult } from "../base/test-connection/test-connection-service"; import type { IMediaServerIntegration } from "../interfaces/media-server/media-server-integration"; import type { CurrentSessionsInput, StreamSession } from "../interfaces/media-server/media-server-types"; +import type { IMediaReleasesIntegration, MediaRelease } from "../types"; import type { PlexResponse } from "./interface"; -export class PlexIntegration extends Integration implements IMediaServerIntegration { +export class PlexIntegration extends Integration implements IMediaServerIntegration, IMediaReleasesIntegration { public async getCurrentSessionsAsync(_options: CurrentSessionsInput): Promise { const token = super.getSecretValue("apiKey"); @@ -66,6 +69,93 @@ export class PlexIntegration extends Integration implements IMediaServerIntegrat return medias; } + public async getMediaReleasesAsync(): Promise { + const token = super.getSecretValue("apiKey"); + const machineIdentifier = await this.getMachineIdentifierAsync(); + const response = await fetchWithTrustedCertificatesAsync(super.url("/library/recentlyAdded"), { + headers: { + "X-Plex-Token": token, + Accept: "application/json", + }, + }); + + const data = await recentlyAddedSchema.parseAsync(await response.json()); + const imageProxy = new ImageProxy(); + + const images = + data.MediaContainer.Metadata?.flatMap((item) => [ + { + mediaKey: item.key, + type: "poster", + url: item.Image.find((image) => image?.type === "coverPoster")?.url, + }, + { + mediaKey: item.key, + type: "backdrop", + url: item.Image.find((image) => image?.type === "background")?.url, + }, + ]).filter( + (image): image is { mediaKey: string; type: "poster" | "backdrop"; url: string } => image.url !== undefined, + ) ?? []; + + const proxiedImages = await Promise.all( + images.map(async (image) => { + const imageUrl = super.url(image.url as `/${string}`); + const proxiedImageUrl = await imageProxy + .createImageAsync(imageUrl.toString(), { + "X-Plex-Token": token, + }) + .catch((error) => { + logger.debug(new Error("Failed to proxy image", { cause: error })); + return undefined; + }); + return { + mediaKey: image.mediaKey, + type: image.type, + url: proxiedImageUrl, + }; + }), + ); + + return ( + data.MediaContainer.Metadata?.map((item) => { + return { + id: item.Media.at(0)?.id.toString() ?? item.key, + type: item.type === "movie" ? "movie" : item.type === "tv" ? "tv" : "unknown", + title: item.title, + subtitle: item.tagline, + description: item.summary, + releaseDate: item.originallyAvailableAt + ? new Date(item.originallyAvailableAt) + : new Date(item.addedAt * 1000), + imageUrls: { + poster: proxiedImages.find((image) => image.mediaKey === item.key && image.type === "poster")?.url, + backdrop: proxiedImages.find((image) => image.mediaKey === item.key && image.type === "backdrop")?.url, + }, + producer: item.studio, + rating: item.rating?.toFixed(1), + tags: item.Genre.map((genre) => genre.tag), + href: super + .url(`/web/index.html#!/server/${machineIdentifier}/details?key=${encodeURIComponent(item.key)}`) + .toString(), + length: item.duration ? Math.round(item.duration / 1000) : undefined, + }; + }) ?? [] + ); + } + + private async getMachineIdentifierAsync(): Promise { + const token = super.getSecretValue("apiKey"); + const response = await fetchWithTrustedCertificatesAsync(super.url("/identity"), { + headers: { + "X-Plex-Token": token, + Accept: "application/json", + }, + }); + const data = await identitySchema.parseAsync(await response.json()); + return data.MediaContainer.machineIdentifier; + } + protected async testingAsync(input: IntegrationTestingInput): Promise { const token = super.getSecretValue("apiKey"); @@ -111,3 +201,50 @@ export class PlexIntegration extends Integration implements IMediaServerIntegrat } } } + +// https://plexapi.dev/api-reference/library/get-recently-added +const recentlyAddedSchema = z.object({ + MediaContainer: z.object({ + Metadata: z + .array( + z.object({ + key: z.string(), + studio: z.string().optional(), + type: z.string(), // For example "movie" + title: z.string(), + summary: z.string().optional(), + duration: z.number().optional(), + addedAt: z.number(), + rating: z.number().optional(), + tagline: z.string().optional(), + originallyAvailableAt: z.string().optional(), + Media: z.array( + z.object({ + id: z.number(), + }), + ), + Image: z.array( + z + .object({ + type: z.string(), // for example "coverPoster" or "background" + url: z.string(), + }) + .optional(), + ), + Genre: z.array( + z.object({ + tag: z.string(), + }), + ), + }), + ) + .optional(), + }), +}); + +// https://plexapi.dev/api-reference/server/get-server-identity +const identitySchema = z.object({ + MediaContainer: z.object({ + machineIdentifier: z.string(), + }), +}); diff --git a/packages/integrations/src/types.ts b/packages/integrations/src/types.ts index b8dc464d4..827c100b3 100644 --- a/packages/integrations/src/types.ts +++ b/packages/integrations/src/types.ts @@ -8,3 +8,4 @@ export * from "./base/searchable-integration"; export * from "./homeassistant/homeassistant-types"; export * from "./proxmox/proxmox-types"; export * from "./unifi-controller/unifi-controller-types"; +export * from "./interfaces/media-releases"; diff --git a/packages/log/package.json b/packages/log/package.json index 8b0825d51..e53a7bd05 100644 --- a/packages/log/package.json +++ b/packages/log/package.json @@ -24,8 +24,7 @@ }, "prettier": "@homarr/prettier-config", "dependencies": { - "@homarr/env": "workspace:^0.1.0", - "ioredis": "5.6.1", + "@homarr/core": "workspace:^0.1.0", "superjson": "2.2.2", "winston": "3.17.0", "zod": "^3.25.76" diff --git a/packages/log/src/env.ts b/packages/log/src/env.ts index a871914d2..cd6ea02d4 100644 --- a/packages/log/src/env.ts +++ b/packages/log/src/env.ts @@ -1,6 +1,6 @@ import { z } from "zod"; -import { createEnv } from "@homarr/env"; +import { createEnv } from "@homarr/core/infrastructure/env"; import { logLevels } from "./constants"; diff --git a/packages/log/src/redis-transport.ts b/packages/log/src/redis-transport.ts index 4ca77ff2f..a674803bd 100644 --- a/packages/log/src/redis-transport.ts +++ b/packages/log/src/redis-transport.ts @@ -1,7 +1,9 @@ -import { Redis } from "ioredis"; import superjson from "superjson"; import Transport from "winston-transport"; +import type { RedisClient } from "@homarr/core/infrastructure/redis"; +import { createRedisClient } from "@homarr/core/infrastructure/redis"; + const messageSymbol = Symbol.for("message"); const levelSymbol = Symbol.for("level"); @@ -10,7 +12,7 @@ const levelSymbol = Symbol.for("level"); // of the base functionality and `.exceptions.handle()`. // export class RedisTransport extends Transport { - private redis: Redis | null = null; + private redis: RedisClient | null = null; /** * Log the info to the Redis channel @@ -21,7 +23,7 @@ export class RedisTransport extends Transport { }); // Is only initialized here because it did not work when initialized in the constructor or outside the class - this.redis ??= new Redis(); + this.redis ??= createRedisClient(); this.redis .publish( diff --git a/packages/modals-collection/package.json b/packages/modals-collection/package.json index 7aa2f8f7a..945cc4ed6 100644 --- a/packages/modals-collection/package.json +++ b/packages/modals-collection/package.json @@ -34,9 +34,9 @@ "@homarr/ui": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0", "@mantine/core": "^8.1.3", - "@tabler/icons-react": "^3.34.0", + "@tabler/icons-react": "^3.34.1", "dayjs": "^1.11.13", - "next": "15.4.1", + "next": "15.4.2", "react": "19.1.0", "react-dom": "19.1.0", "zod": "^3.25.76" diff --git a/packages/notifications/package.json b/packages/notifications/package.json index 4c4f09102..44f09359b 100644 --- a/packages/notifications/package.json +++ b/packages/notifications/package.json @@ -25,7 +25,7 @@ "dependencies": { "@homarr/ui": "workspace:^0.1.0", "@mantine/notifications": "^8.1.3", - "@tabler/icons-react": "^3.34.0" + "@tabler/icons-react": "^3.34.1" }, "devDependencies": { "@homarr/eslint-config": "workspace:^0.2.0", diff --git a/packages/old-import/package.json b/packages/old-import/package.json index a3e9e4af3..0d6c5db1c 100644 --- a/packages/old-import/package.json +++ b/packages/old-import/package.json @@ -40,7 +40,7 @@ "@mantine/core": "^8.1.3", "@mantine/hooks": "^8.1.3", "adm-zip": "0.5.16", - "next": "15.4.1", + "next": "15.4.2", "react": "19.1.0", "react-dom": "19.1.0", "superjson": "2.2.2", diff --git a/packages/redis/package.json b/packages/redis/package.json index 77ab66b07..37624a09b 100644 --- a/packages/redis/package.json +++ b/packages/redis/package.json @@ -23,6 +23,8 @@ "prettier": "@homarr/prettier-config", "dependencies": { "@homarr/common": "workspace:^", + "@homarr/core": "workspace:^", + "@homarr/db": "workspace:^", "@homarr/definitions": "workspace:^", "@homarr/log": "workspace:^", "ioredis": "5.6.1", diff --git a/packages/redis/src/lib/connection.ts b/packages/redis/src/lib/connection.ts index af6c05fac..ba945de33 100644 --- a/packages/redis/src/lib/connection.ts +++ b/packages/redis/src/lib/connection.ts @@ -1,4 +1,5 @@ -import { Redis } from "ioredis"; +import type { RedisClient } from "@homarr/core/infrastructure/redis"; +import { createRedisClient } from "@homarr/core/infrastructure/redis"; /** * Creates a new Redis connection @@ -7,8 +8,8 @@ import { Redis } from "ioredis"; export const createRedisConnection = () => { if (Boolean(process.env.CI) || Boolean(process.env.DISABLE_REDIS_LOGS)) { // Return null if we are in CI as we don't want to connect to Redis - return null as unknown as Redis; + return null as unknown as RedisClient; } - return new Redis(); + return createRedisClient(); }; diff --git a/packages/request-handler/src/media-release.ts b/packages/request-handler/src/media-release.ts new file mode 100644 index 000000000..c0ffe48f3 --- /dev/null +++ b/packages/request-handler/src/media-release.ts @@ -0,0 +1,20 @@ +import dayjs from "dayjs"; + +import type { IntegrationKindByCategory } from "@homarr/definitions"; +import { createIntegrationAsync } from "@homarr/integrations"; +import type { MediaRelease } from "@homarr/integrations/types"; + +import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; + +export const mediaReleaseRequestHandler = createCachedIntegrationRequestHandler< + MediaRelease[], + IntegrationKindByCategory<"mediaRelease">, + Record +>({ + async requestAsync(integration, _input) { + const integrationInstance = await createIntegrationAsync(integration); + return await integrationInstance.getMediaReleasesAsync(); + }, + cacheDuration: dayjs.duration(5, "minutes"), + queryKey: "mediaReleases", +}); diff --git a/packages/settings/package.json b/packages/settings/package.json index 80f761233..bb6bd7d8c 100644 --- a/packages/settings/package.json +++ b/packages/settings/package.json @@ -27,7 +27,7 @@ "@homarr/db": "workspace:^0.1.0", "@homarr/server-settings": "workspace:^0.1.0", "@mantine/dates": "^8.1.3", - "next": "15.4.1", + "next": "15.4.2", "react": "19.1.0", "react-dom": "19.1.0" }, diff --git a/packages/spotlight/package.json b/packages/spotlight/package.json index 9ccce4b4c..4c73cd086 100644 --- a/packages/spotlight/package.json +++ b/packages/spotlight/package.json @@ -36,9 +36,9 @@ "@mantine/core": "^8.1.3", "@mantine/hooks": "^8.1.3", "@mantine/spotlight": "^8.1.3", - "@tabler/icons-react": "^3.34.0", + "@tabler/icons-react": "^3.34.1", "jotai": "^2.12.5", - "next": "15.4.1", + "next": "15.4.2", "react": "19.1.0", "react-dom": "19.1.0", "use-deep-compare-effect": "^1.8.1" diff --git a/packages/translation/package.json b/packages/translation/package.json index c36730612..2baf1d489 100644 --- a/packages/translation/package.json +++ b/packages/translation/package.json @@ -32,7 +32,7 @@ "dayjs": "^1.11.13", "deepmerge": "4.3.1", "mantine-react-table": "2.0.0-beta.9", - "next": "15.4.1", + "next": "15.4.2", "next-intl": "4.3.4", "react": "19.1.0", "react-dom": "19.1.0" diff --git a/packages/translation/src/lang/en.json b/packages/translation/src/lang/en.json index b09f08258..5df3d35d6 100644 --- a/packages/translation/src/lang/en.json +++ b/packages/translation/src/lang/en.json @@ -2079,6 +2079,35 @@ }, "globalRatio": "Global Ratio" }, + "mediaReleases": { + "name": "Media releases", + "description": "Display newly added medias or upcoming releases from different integrations", + "option": { + "layout": { + "label": "Layout", + "option": { + "backdrop": { + "label": "Backdrop" + }, + "poster": { + "label": "Poster" + } + } + }, + "showDescriptionTooltip": { + "label": "Show description tooltip" + }, + "showType": { + "label": "Show media type badge" + }, + "showSource": { + "label": "Show source integration" + } + }, + "length": { + "duration": "{length}min" + } + }, "mediaRequests-requestList": { "name": "Media Requests List", "description": "See a list of all media requests from your Overseerr or Jellyseerr instance", diff --git a/packages/ui/package.json b/packages/ui/package.json index 72ade1ec0..a7dbeb7d7 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -33,9 +33,9 @@ "@mantine/core": "^8.1.3", "@mantine/dates": "^8.1.3", "@mantine/hooks": "^8.1.3", - "@tabler/icons-react": "^3.34.0", + "@tabler/icons-react": "^3.34.1", "mantine-react-table": "2.0.0-beta.9", - "next": "15.4.1", + "next": "15.4.2", "react": "19.1.0", "react-dom": "19.1.0", "svgson": "^5.3.1" diff --git a/packages/ui/src/components/overflow-badge.tsx b/packages/ui/src/components/overflow-badge.tsx index 976f8e2fd..253597ea8 100644 --- a/packages/ui/src/components/overflow-badge.tsx +++ b/packages/ui/src/components/overflow-badge.tsx @@ -1,13 +1,17 @@ -import type { BadgeProps } from "@mantine/core"; -import { ActionIcon, Badge, Group, Popover, Stack } from "@mantine/core"; +import type { BadgeProps, MantineSpacing } from "@mantine/core"; +import { Badge, Group, Popover, Stack, UnstyledButton } from "@mantine/core"; export function OverflowBadge({ data, overflowCount = 3, + disablePopover = false, + groupGap = "xs", ...props }: { data: string[]; overflowCount?: number; + disablePopover?: boolean; + groupGap?: MantineSpacing; } & BadgeProps) { const badgeProps = { variant: "default", @@ -16,8 +20,8 @@ export function OverflowBadge({ ...props, }; return ( - - + + {data.slice(0, overflowCount).map((item) => ( {item} @@ -25,19 +29,11 @@ export function OverflowBadge({ ))} {data.length > overflowCount && ( - - +{data.length - overflowCount} - + + + +{data.length - overflowCount} + + )} diff --git a/packages/widgets/package.json b/packages/widgets/package.json index aa4779b7c..1ae4a995b 100644 --- a/packages/widgets/package.json +++ b/packages/widgets/package.json @@ -51,7 +51,7 @@ "@mantine/charts": "^8.1.3", "@mantine/core": "^8.1.3", "@mantine/hooks": "^8.1.3", - "@tabler/icons-react": "^3.34.0", + "@tabler/icons-react": "^3.34.1", "@tiptap/extension-color": "2.26.1", "@tiptap/extension-highlight": "2.26.1", "@tiptap/extension-image": "2.26.1", @@ -71,7 +71,7 @@ "clsx": "^2.1.1", "dayjs": "^1.11.13", "mantine-react-table": "2.0.0-beta.9", - "next": "15.4.1", + "next": "15.4.2", "react": "19.1.0", "react-dom": "19.1.0", "react-markdown": "^10.1.0", diff --git a/packages/widgets/src/_inputs/widget-multiReleasesRepositories-input.tsx b/packages/widgets/src/_inputs/widget-multiReleasesRepositories-input.tsx index 0eecc8e2e..556322084 100644 --- a/packages/widgets/src/_inputs/widget-multiReleasesRepositories-input.tsx +++ b/packages/widgets/src/_inputs/widget-multiReleasesRepositories-input.tsx @@ -509,19 +509,19 @@ interface ReleasesRepositoryImport extends ReleasesRepository { alreadyImported: boolean; } -interface ContainerImageSelectorProps { - containerImage: ReleasesRepositoryImport; +interface ImportRepositorySelectProps { + repository: ReleasesRepositoryImport; integration?: Integration; versionFilterPrecisionOptions: string[]; onImageSelectionChanged?: (isSelected: boolean) => void; } -const ContainerImageSelector = ({ - containerImage, +const ImportRepositorySelect = ({ + repository, integration, versionFilterPrecisionOptions, onImageSelectionChanged, -}: ContainerImageSelectorProps) => { +}: ImportRepositorySelectProps) => { const tRepository = useScopedI18n("widget.releases.option.repositories"); const checkBoxProps: CheckboxProps = !onImageSelectionChanged ? { @@ -539,29 +539,29 @@ const ContainerImageSelector = ({ label={ - {containerImage.identifier} + {repository.identifier} } {...checkBoxProps} /> - {containerImage.versionFilter && ( + {repository.versionFilter && ( {tRepository("versionFilter.label")}: - {containerImage.versionFilter.prefix && containerImage.versionFilter.prefix} + {repository.versionFilter.prefix && repository.versionFilter.prefix} - {versionFilterPrecisionOptions[containerImage.versionFilter.precision]} + {versionFilterPrecisionOptions[repository.versionFilter.precision]} - {containerImage.versionFilter.suffix && containerImage.versionFilter.suffix} + {repository.versionFilter.suffix && repository.versionFilter.suffix} )} @@ -610,36 +610,47 @@ const RepositoryImportModal = createModal(({ innerProps, enabled: innerProps.isAdmin, }); - const containersImages: ReleasesRepositoryImport[] = useMemo( + const importRepositories: ReleasesRepositoryImport[] = useMemo( () => - docker.data?.containers.reduce((acc, containerImage) => { - const imageParts = containerImage.image.split("/"); - const source = imageParts.length > 1 ? imageParts[0] : "docker.io"; - const identifierImage = imageParts.length > 1 ? imageParts[1] : imageParts[0]; + docker.data?.containers.reduce((acc, container) => { + const [maybeSource, maybeIdentifierAndVersion] = container.image.split(/\/(.*)/); + const hasSource = maybeSource && maybeSource in sourceToProviderKind; + const source = hasSource ? maybeSource : "docker.io"; + const identifierAndVersion = hasSource ? maybeIdentifierAndVersion : container.image; - if (!source || !identifierImage) return acc; + if (!identifierAndVersion) return acc; - const providerKey = source in containerImageToProviderKind ? containerImageToProviderKind[source] : "dockerHub"; + const providerKey = sourceToProviderKind[source]; const integrationId = Object.values(innerProps.integrations).find( (integration) => integration.kind === providerKey, )?.id; - const [identifier, version] = identifierImage.split(":"); + const [identifier, version] = identifierAndVersion.split(":"); if (!identifier || !integrationId) return acc; - if (acc.some((item) => item.providerIntegrationId === integrationId && item.identifier === identifier)) + if ( + acc.some( + (item) => + item.providerIntegrationId !== undefined && + innerProps.integrations[item.providerIntegrationId]?.kind === providerKey && + item.identifier === identifier, + ) + ) return acc; acc.push({ id: createId(), providerIntegrationId: integrationId, identifier, - iconUrl: containerImage.iconUrl ?? undefined, + iconUrl: container.iconUrl ?? undefined, name: formatIdentifierName(identifier), versionFilter: version ? parseImageVersionToVersionFilter(version) : undefined, alreadyImported: innerProps.repositories.some( - (item) => item.providerIntegrationId === integrationId && item.identifier === identifier, + (item) => + item.providerIntegrationId !== undefined && + innerProps.integrations[item.providerIntegrationId]?.kind === providerKey && + item.identifier === identifier, ), }); return acc; @@ -657,13 +668,13 @@ const RepositoryImportModal = createModal(({ innerProps, }, [innerProps, selectedImages, actions]); const allImagesImported = useMemo( - () => containersImages.every((containerImage) => containerImage.alreadyImported), - [containersImages], + () => importRepositories.every((repository) => repository.alreadyImported), + [importRepositories], ); const anyImagesImported = useMemo( - () => containersImages.some((containerImage) => containerImage.alreadyImported), - [containersImages], + () => importRepositories.some((repository) => repository.alreadyImported), + [importRepositories], ); return ( @@ -673,7 +684,7 @@ const RepositoryImportModal = createModal(({ innerProps, {tRepository("importRepositories.loading")} - ) : containersImages.length === 0 ? ( + ) : importRepositories.length === 0 ? ( {tRepository("importRepositories.noImagesFound")} @@ -694,23 +705,23 @@ const RepositoryImportModal = createModal(({ innerProps, {!allImagesImported && - containersImages - .filter((containerImage) => !containerImage.alreadyImported) - .map((containerImage) => { - const integration = containerImage.providerIntegrationId - ? innerProps.integrations[containerImage.providerIntegrationId] + importRepositories + .filter((repository) => !repository.alreadyImported) + .map((repository) => { + const integration = repository.providerIntegrationId + ? innerProps.integrations[repository.providerIntegrationId] : undefined; return ( - isSelected - ? setSelectedImages([...selectedImages, containerImage]) - : setSelectedImages(selectedImages.filter((img) => img !== containerImage)) + ? setSelectedImages([...selectedImages, repository]) + : setSelectedImages(selectedImages.filter((img) => img !== repository)) } /> ); @@ -723,17 +734,17 @@ const RepositoryImportModal = createModal(({ innerProps, {anyImagesImported && - containersImages - .filter((containerImage) => containerImage.alreadyImported) - .map((containerImage) => { - const integration = containerImage.providerIntegrationId - ? innerProps.integrations[containerImage.providerIntegrationId] + importRepositories + .filter((repository) => repository.alreadyImported) + .map((repository) => { + const integration = repository.providerIntegrationId + ? innerProps.integrations[repository.providerIntegrationId] : undefined; return ( - @@ -763,7 +774,7 @@ const RepositoryImportModal = createModal(({ innerProps, size: "xl", }); -const containerImageToProviderKind: Record = { +const sourceToProviderKind: Record = { "ghcr.io": "github", "docker.io": "dockerHub", }; diff --git a/packages/widgets/src/index.tsx b/packages/widgets/src/index.tsx index fc2fbc8e4..91ec26ac3 100644 --- a/packages/widgets/src/index.tsx +++ b/packages/widgets/src/index.tsx @@ -20,6 +20,7 @@ import * as healthMonitoring from "./health-monitoring"; import * as iframe from "./iframe"; import type { WidgetImportRecord } from "./import"; import * as indexerManager from "./indexer-manager"; +import * as mediaReleases from "./media-releases"; import * as mediaRequestsList from "./media-requests/list"; import * as mediaRequestsStats from "./media-requests/stats"; import * as mediaServer from "./media-server"; @@ -69,6 +70,7 @@ export const widgetImports = { dockerContainers, releases, notifications, + mediaReleases, } satisfies WidgetImportRecord; export type WidgetImports = typeof widgetImports; diff --git a/packages/widgets/src/media-releases/component.tsx b/packages/widgets/src/media-releases/component.tsx new file mode 100644 index 000000000..cd2432ddb --- /dev/null +++ b/packages/widgets/src/media-releases/component.tsx @@ -0,0 +1,206 @@ +"use client"; + +import { Fragment } from "react"; +import { Avatar, Badge, Box, Divider, Group, Image, Stack, Text, TooltipFloating, UnstyledButton } from "@mantine/core"; +import { IconBook, IconCalendar, IconClock, IconStarFilled } from "@tabler/icons-react"; + +import type { RouterOutputs } from "@homarr/api"; +import { clientApi } from "@homarr/api/client"; +import { getMantineColor } from "@homarr/common"; +import { getIconUrl } from "@homarr/definitions"; +import type { MediaRelease } from "@homarr/integrations/types"; +import { mediaTypeConfigurations } from "@homarr/integrations/types"; +import type { TranslationFunction } from "@homarr/translation"; +import { useCurrentLocale, useI18n } from "@homarr/translation/client"; +import type { TablerIcon } from "@homarr/ui"; +import { OverflowBadge } from "@homarr/ui"; + +import type { WidgetComponentProps } from "../definition"; + +export default function MediaReleasesWidget({ options, integrationIds }: WidgetComponentProps<"mediaReleases">) { + const [releases] = clientApi.widget.mediaRelease.getMediaReleases.useSuspenseQuery({ + integrationIds, + }); + + return ( + + {releases.map((item, index) => ( + + {index !== 0 && options.layout === "poster" && } + + + ))} + + ); +} + +interface ItemProps { + item: RouterOutputs["widget"]["mediaRelease"]["getMediaReleases"][number]; + options: WidgetComponentProps<"mediaReleases">["options"]; +} + +const Item = ({ item, options }: ItemProps) => { + const locale = useCurrentLocale(); + const t = useI18n(); + const length = formatLength(item.length, item.type, t); + + return ( + + + {options.layout === "backdrop" && ( + + )} + + + {options.layout === "poster" && {item.title}} + + + + {item.title} + + {item.subtitle !== undefined && ( + + {item.subtitle} + + )} + + + + {length !== undefined && ( + <> + + + + )} + {item.producer !== undefined && ( + <> + + + + )} + {item.rating !== undefined && ( + <> + + + + )} + {item.price !== undefined && ( + <> + + + + )} + + {item.tags.length > 0 && ( + + )} + + + {(options.showType || options.showSource) && ( + + {options.showType && ( + + {item.type} + + )} + + {options.showSource && ( + + )} + + )} + + + + ); +}; + +interface IconAndLabelProps { + icon?: TablerIcon; + label: string; +} + +const InfoDivider = () => ( + + • + +); + +const Info = ({ icon: Icon, label }: IconAndLabelProps) => { + return ( + + {Icon && } + + {label} + + + ); +}; + +const formatLength = (length: number | undefined, type: MediaRelease["type"], t: TranslationFunction) => { + if (!length) return undefined; + if (type === "movie" || type === "tv" || type === "video" || type === "music" || type === "article") { + return { + type: "duration" as const, + label: t("widget.mediaReleases.length.duration", { + length: Math.round(length / 60).toString(), + }), + }; + } + if (type === "book") { + return { + type: "page" as const, + label: length.toString(), + }; + } + + return undefined; +}; diff --git a/packages/widgets/src/media-releases/index.ts b/packages/widgets/src/media-releases/index.ts new file mode 100644 index 000000000..af650eef7 --- /dev/null +++ b/packages/widgets/src/media-releases/index.ts @@ -0,0 +1,35 @@ +import { IconTicket } from "@tabler/icons-react"; + +import { createWidgetDefinition } from "../definition"; +import { optionsBuilder } from "../options"; + +export const { definition, componentLoader } = createWidgetDefinition("mediaReleases", { + icon: IconTicket, + createOptions() { + return optionsBuilder.from((factory) => ({ + layout: factory.select({ + defaultValue: "backdrop", + options: [ + { + value: "backdrop", + label: (t) => t("widget.mediaReleases.option.layout.option.backdrop.label"), + }, + { + value: "poster", + label: (t) => t("widget.mediaReleases.option.layout.option.poster.label"), + }, + ], + }), + showDescriptionTooltip: factory.switch({ + defaultValue: true, + }), + showType: factory.switch({ + defaultValue: true, + }), + showSource: factory.switch({ + defaultValue: true, + }), + })); + }, + supportedIntegrations: ["mock", "emby", "jellyfin", "plex"], +}).withDynamicImport(() => import("./component")); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 08e6419b3..7608d9e45 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -37,6 +37,9 @@ importers: '@semantic-release/release-notes-generator': specifier: ^14.0.3 version: 14.0.3(semantic-release@24.2.7(typescript@5.8.3)) + '@testcontainers/redis': + specifier: ^11.3.1 + version: 11.3.1 '@turbo/gen': specifier: ^2.5.5 version: 2.5.5(@types/node@22.16.4)(typescript@5.8.3) @@ -65,8 +68,8 @@ importers: specifier: ^24.2.7 version: 24.2.7(typescript@5.8.3) testcontainers: - specifier: ^11.2.1 - version: 11.2.1 + specifier: ^11.3.1 + version: 11.3.1 turbo: specifier: ^2.5.5 version: 2.5.5 @@ -112,6 +115,9 @@ importers: '@homarr/common': specifier: workspace:^0.1.0 version: link:../../packages/common + '@homarr/core': + specifier: workspace:^0.1.0 + version: link:../../packages/core '@homarr/cron-job-status': specifier: workspace:^0.1.0 version: link:../../packages/cron-job-status @@ -124,9 +130,6 @@ importers: '@homarr/docker': specifier: workspace:^0.1.0 version: link:../../packages/docker - '@homarr/env': - specifier: workspace:^0.1.0 - version: link:../../packages/env '@homarr/form': specifier: workspace:^0.1.0 version: link:../../packages/form @@ -139,6 +142,9 @@ importers: '@homarr/icons': specifier: workspace:^0.1.0 version: link:../../packages/icons + '@homarr/image-proxy': + specifier: workspace:^0.1.0 + version: link:../../packages/image-proxy '@homarr/integrations': specifier: workspace:^0.1.0 version: link:../../packages/integrations @@ -206,8 +212,8 @@ importers: specifier: 1.0.14 version: 1.0.14(rollup@4.21.3)(webpack-sources@3.2.3) '@tabler/icons-react': - specifier: ^3.34.0 - version: 3.34.0(react@19.1.0) + specifier: ^3.34.1 + version: 3.34.1(react@19.1.0) '@tanstack/react-query': specifier: ^5.83.0 version: 5.83.0(react@19.1.0) @@ -216,13 +222,13 @@ importers: version: 5.83.0(@tanstack/react-query@5.83.0(react@19.1.0))(react@19.1.0) '@tanstack/react-query-next-experimental': specifier: ^5.83.0 - version: 5.83.0(@tanstack/react-query@5.83.0(react@19.1.0))(next@15.4.1(@babel/core@7.26.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.89.2))(react@19.1.0) + version: 5.83.0(@tanstack/react-query@5.83.0(react@19.1.0))(next@15.4.2(@babel/core@7.26.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.89.2))(react@19.1.0) '@trpc/client': specifier: ^11.4.3 version: 11.4.3(@trpc/server@11.4.3(typescript@5.8.3))(typescript@5.8.3) '@trpc/next': specifier: ^11.4.3 - version: 11.4.3(@tanstack/react-query@5.83.0(react@19.1.0))(@trpc/client@11.4.3(@trpc/server@11.4.3(typescript@5.8.3))(typescript@5.8.3))(@trpc/react-query@11.4.3(@tanstack/react-query@5.83.0(react@19.1.0))(@trpc/client@11.4.3(@trpc/server@11.4.3(typescript@5.8.3))(typescript@5.8.3))(@trpc/server@11.4.3(typescript@5.8.3))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.8.3))(@trpc/server@11.4.3(typescript@5.8.3))(next@15.4.1(@babel/core@7.26.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.89.2))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.8.3) + version: 11.4.3(@tanstack/react-query@5.83.0(react@19.1.0))(@trpc/client@11.4.3(@trpc/server@11.4.3(typescript@5.8.3))(typescript@5.8.3))(@trpc/react-query@11.4.3(@tanstack/react-query@5.83.0(react@19.1.0))(@trpc/client@11.4.3(@trpc/server@11.4.3(typescript@5.8.3))(typescript@5.8.3))(@trpc/server@11.4.3(typescript@5.8.3))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.8.3))(@trpc/server@11.4.3(typescript@5.8.3))(next@15.4.2(@babel/core@7.26.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.89.2))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.8.3) '@trpc/react-query': specifier: ^11.4.3 version: 11.4.3(@tanstack/react-query@5.83.0(react@19.1.0))(@trpc/client@11.4.3(@trpc/server@11.4.3(typescript@5.8.3))(typescript@5.8.3))(@trpc/server@11.4.3(typescript@5.8.3))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.8.3) @@ -261,10 +267,10 @@ importers: version: 2.12.5(@types/react@19.1.8)(react@19.1.0) mantine-react-table: specifier: 2.0.0-beta.9 - version: 2.0.0-beta.9(@mantine/core@8.1.3(@mantine/hooks@8.1.3(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@mantine/dates@8.1.3(@mantine/core@8.1.3(@mantine/hooks@8.1.3(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@mantine/hooks@8.1.3(react@19.1.0))(dayjs@1.11.13)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@mantine/hooks@8.1.3(react@19.1.0))(@tabler/icons-react@3.34.0(react@19.1.0))(clsx@2.1.1)(dayjs@1.11.13)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + version: 2.0.0-beta.9(@mantine/core@8.1.3(@mantine/hooks@8.1.3(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@mantine/dates@8.1.3(@mantine/core@8.1.3(@mantine/hooks@8.1.3(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@mantine/hooks@8.1.3(react@19.1.0))(dayjs@1.11.13)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@mantine/hooks@8.1.3(react@19.1.0))(@tabler/icons-react@3.34.1(react@19.1.0))(clsx@2.1.1)(dayjs@1.11.13)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) next: - specifier: 15.4.1 - version: 15.4.1(@babel/core@7.26.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.89.2) + specifier: 15.4.2 + version: 15.4.2(@babel/core@7.26.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.89.2) postcss-preset-mantine: specifier: ^1.18.0 version: 1.18.0(postcss@8.4.47) @@ -418,8 +424,8 @@ importers: specifier: ^8.0.0 version: 8.0.0 esbuild: - specifier: ^0.25.6 - version: 0.25.6 + specifier: ^0.25.8 + version: 0.25.8 eslint: specifier: ^9.31.0 version: 9.31.0 @@ -482,8 +488,8 @@ importers: specifier: ^8.18.1 version: 8.18.1 esbuild: - specifier: ^0.25.6 - version: 0.25.6 + specifier: ^0.25.8 + version: 0.25.8 eslint: specifier: ^9.31.0 version: 9.31.0 @@ -606,8 +612,8 @@ importers: specifier: ^4.5.0 version: 4.5.0 next: - specifier: 15.4.1 - version: 15.4.1(@babel/core@7.28.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.89.2) + specifier: 15.4.2 + version: 15.4.2(@babel/core@7.28.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.89.2) react: specifier: 19.1.0 version: 19.1.0 @@ -657,15 +663,15 @@ importers: '@homarr/common': specifier: workspace:^0.1.0 version: link:../common + '@homarr/core': + specifier: workspace:^0.1.0 + version: link:../core '@homarr/db': specifier: workspace:^0.1.0 version: link:../db '@homarr/definitions': specifier: workspace:^0.1.0 version: link:../definitions - '@homarr/env': - specifier: workspace:^0.1.0 - version: link:../env '@homarr/log': specifier: workspace:^0.1.0 version: link:../log @@ -682,11 +688,11 @@ importers: specifier: 8.0.6 version: 8.0.6 next: - specifier: 15.4.1 - version: 15.4.1(@babel/core@7.28.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.89.2) + specifier: 15.4.2 + version: 15.4.2(@babel/core@7.28.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.89.2) next-auth: specifier: 5.0.0-beta.29 - version: 5.0.0-beta.29(next@15.4.1(@babel/core@7.28.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.89.2))(react@19.1.0) + version: 5.0.0-beta.29(next@15.4.2(@babel/core@7.28.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.89.2))(react@19.1.0) react: specifier: 19.1.0 version: 19.1.0 @@ -809,8 +815,8 @@ importers: specifier: workspace:^0.1.0 version: link:../../tooling/typescript esbuild: - specifier: ^0.25.6 - version: 0.25.6 + specifier: ^0.25.8 + version: 0.25.8 eslint: specifier: ^9.31.0 version: 9.31.0 @@ -820,9 +826,9 @@ importers: packages/common: dependencies: - '@homarr/env': + '@homarr/core': specifier: workspace:^0.1.0 - version: link:../env + version: link:../core '@homarr/log': specifier: workspace:^0.1.0 version: link:../log @@ -833,8 +839,8 @@ importers: specifier: ^1.11.13 version: 1.11.13 next: - specifier: 15.4.1 - version: 15.4.1(@babel/core@7.28.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.89.2) + specifier: 15.4.2 + version: 15.4.2(@babel/core@7.28.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.89.2) react: specifier: 19.1.0 version: 19.1.0 @@ -867,17 +873,45 @@ importers: specifier: ^5.8.3 version: 5.8.3 + packages/core: + dependencies: + '@t3-oss/env-nextjs': + specifier: ^0.13.8 + version: 0.13.8(arktype@2.1.20)(typescript@5.8.3)(zod@3.25.76) + ioredis: + specifier: 5.6.1 + version: 5.6.1 + zod: + specifier: ^3.25.76 + version: 3.25.76 + devDependencies: + '@homarr/eslint-config': + specifier: workspace:^0.2.0 + version: link:../../tooling/eslint + '@homarr/prettier-config': + specifier: workspace:^0.1.0 + version: link:../../tooling/prettier + '@homarr/tsconfig': + specifier: workspace:^0.1.0 + version: link:../../tooling/typescript + eslint: + specifier: ^9.31.0 + version: 9.31.0 + typescript: + specifier: ^5.8.3 + version: 5.8.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 - '@homarr/env': - specifier: workspace:^0.1.0 - version: link:../env '@homarr/log': specifier: workspace:^0.1.0 version: link:../log @@ -1053,12 +1087,12 @@ importers: '@homarr/common': specifier: workspace:^0.1.0 version: link:../common + '@homarr/core': + specifier: workspace:^0.1.0 + version: link:../core '@homarr/definitions': specifier: workspace:^0.1.0 version: link:../definitions - '@homarr/env': - specifier: workspace:^0.1.0 - version: link:../env '@homarr/log': specifier: workspace:^0.1.0 version: link:../log @@ -1072,8 +1106,8 @@ importers: specifier: ^2.2.2 version: 2.2.2 '@testcontainers/mysql': - specifier: ^11.2.1 - version: 11.2.1 + specifier: ^11.3.1 + version: 11.3.1 better-sqlite3: specifier: ^12.2.0 version: 12.2.0 @@ -1112,8 +1146,8 @@ importers: specifier: ^8.0.0 version: 8.0.0 esbuild: - specifier: ^0.25.6 - version: 0.25.6 + specifier: ^0.25.8 + version: 0.25.8 eslint: specifier: ^9.31.0 version: 9.31.0 @@ -1163,9 +1197,9 @@ importers: '@homarr/common': specifier: workspace:^0.1.0 version: link:../common - '@homarr/env': + '@homarr/core': specifier: workspace:^0.1.0 - version: link:../env + version: link:../core dockerode: specifier: ^4.0.7 version: 4.0.7 @@ -1189,31 +1223,6 @@ importers: specifier: ^5.8.3 version: 5.8.3 - packages/env: - dependencies: - '@t3-oss/env-nextjs': - specifier: ^0.13.8 - version: 0.13.8(arktype@2.1.20)(typescript@5.8.3)(zod@3.25.76) - zod: - specifier: ^3.25.76 - version: 3.25.76 - devDependencies: - '@homarr/eslint-config': - specifier: workspace:^0.2.0 - version: link:../../tooling/eslint - '@homarr/prettier-config': - specifier: workspace:^0.1.0 - version: link:../../tooling/prettier - '@homarr/tsconfig': - specifier: workspace:^0.1.0 - version: link:../../tooling/typescript - eslint: - specifier: ^9.31.0 - version: 9.31.0 - typescript: - specifier: ^5.8.3 - version: 5.8.3 - packages/form: dependencies: '@homarr/common': @@ -1325,6 +1334,43 @@ importers: specifier: ^5.8.3 version: 5.8.3 + packages/image-proxy: + dependencies: + '@homarr/certificates': + specifier: workspace:^0.1.0 + version: link:../certificates + '@homarr/common': + specifier: workspace:^0.1.0 + version: link:../common + '@homarr/log': + specifier: workspace:^0.1.0 + version: link:../log + '@homarr/redis': + specifier: workspace:^0.1.0 + version: link:../redis + bcrypt: + specifier: ^6.0.0 + version: 6.0.0 + 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/bcrypt': + specifier: 5.0.2 + version: 5.0.2 + eslint: + specifier: ^9.31.0 + version: 9.31.0 + typescript: + specifier: ^5.8.3 + version: 5.8.3 + packages/integrations: dependencies: '@ctrl/deluge': @@ -1337,8 +1383,8 @@ importers: specifier: ^7.2.0 version: 7.2.0 '@gitbeaker/rest': - specifier: ^42.5.0 - version: 42.5.0 + specifier: ^43.3.0 + version: 43.3.0 '@homarr/certificates': specifier: workspace:^0.1.0 version: link:../certificates @@ -1351,6 +1397,9 @@ importers: '@homarr/definitions': specifier: workspace:^0.1.0 version: link:../definitions + '@homarr/image-proxy': + specifier: workspace:^0.1.0 + version: link:../image-proxy '@homarr/log': specifier: workspace:^0.1.0 version: link:../log @@ -1418,12 +1467,9 @@ importers: packages/log: dependencies: - '@homarr/env': + '@homarr/core': specifier: workspace:^0.1.0 - version: link:../env - ioredis: - specifier: 5.6.1 - version: 5.6.1 + version: link:../core superjson: specifier: 2.2.2 version: 2.2.2 @@ -1523,14 +1569,14 @@ importers: specifier: ^8.1.3 version: 8.1.3(@mantine/hooks@8.1.3(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@tabler/icons-react': - specifier: ^3.34.0 - version: 3.34.0(react@19.1.0) + specifier: ^3.34.1 + version: 3.34.1(react@19.1.0) dayjs: specifier: ^1.11.13 version: 1.11.13 next: - specifier: 15.4.1 - version: 15.4.1(@babel/core@7.28.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.89.2) + specifier: 15.4.2 + version: 15.4.2(@babel/core@7.28.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.89.2) react: specifier: 19.1.0 version: 19.1.0 @@ -1566,8 +1612,8 @@ importers: specifier: ^8.1.3 version: 8.1.3(@mantine/core@8.1.3(@mantine/hooks@8.1.3(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@mantine/hooks@8.1.3(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@tabler/icons-react': - specifier: ^3.34.0 - version: 3.34.0(react@19.1.0) + specifier: ^3.34.1 + version: 3.34.1(react@19.1.0) devDependencies: '@homarr/eslint-config': specifier: workspace:^0.2.0 @@ -1630,8 +1676,8 @@ importers: specifier: 0.5.16 version: 0.5.16 next: - specifier: 15.4.1 - version: 15.4.1(@babel/core@7.28.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.89.2) + specifier: 15.4.2 + version: 15.4.2(@babel/core@7.28.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.89.2) react: specifier: 19.1.0 version: 19.1.0 @@ -1697,6 +1743,12 @@ importers: '@homarr/common': specifier: workspace:^ version: link:../common + '@homarr/core': + specifier: workspace:^ + version: link:../core + '@homarr/db': + specifier: workspace:^ + version: link:../db '@homarr/definitions': specifier: workspace:^ version: link:../definitions @@ -1818,8 +1870,8 @@ importers: specifier: ^8.1.3 version: 8.1.3(@mantine/core@8.1.3(@mantine/hooks@8.1.3(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@mantine/hooks@8.1.3(react@19.1.0))(dayjs@1.11.13)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) next: - specifier: 15.4.1 - version: 15.4.1(@babel/core@7.28.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.89.2) + specifier: 15.4.2 + version: 15.4.2(@babel/core@7.28.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.89.2) react: specifier: 19.1.0 version: 19.1.0 @@ -1885,14 +1937,14 @@ importers: specifier: ^8.1.3 version: 8.1.3(@mantine/core@8.1.3(@mantine/hooks@8.1.3(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@mantine/hooks@8.1.3(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@tabler/icons-react': - specifier: ^3.34.0 - version: 3.34.0(react@19.1.0) + specifier: ^3.34.1 + version: 3.34.1(react@19.1.0) jotai: specifier: ^2.12.5 version: 2.12.5(@types/react@19.1.8)(react@19.1.0) next: - specifier: 15.4.1 - version: 15.4.1(@babel/core@7.28.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.89.2) + specifier: 15.4.2 + version: 15.4.2(@babel/core@7.28.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.89.2) react: specifier: 19.1.0 version: 19.1.0 @@ -1935,13 +1987,13 @@ importers: version: 4.3.1 mantine-react-table: specifier: 2.0.0-beta.9 - version: 2.0.0-beta.9(@mantine/core@8.1.3(@mantine/hooks@8.1.3(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@mantine/dates@8.1.3(@mantine/core@8.1.3(@mantine/hooks@8.1.3(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@mantine/hooks@8.1.3(react@19.1.0))(dayjs@1.11.13)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@mantine/hooks@8.1.3(react@19.1.0))(@tabler/icons-react@3.34.0(react@19.1.0))(clsx@2.1.1)(dayjs@1.11.13)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + version: 2.0.0-beta.9(@mantine/core@8.1.3(@mantine/hooks@8.1.3(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@mantine/dates@8.1.3(@mantine/core@8.1.3(@mantine/hooks@8.1.3(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@mantine/hooks@8.1.3(react@19.1.0))(dayjs@1.11.13)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@mantine/hooks@8.1.3(react@19.1.0))(@tabler/icons-react@3.34.1(react@19.1.0))(clsx@2.1.1)(dayjs@1.11.13)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) next: - specifier: 15.4.1 - version: 15.4.1(@babel/core@7.28.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.89.2) + specifier: 15.4.2 + version: 15.4.2(@babel/core@7.28.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.89.2) next-intl: specifier: 4.3.4 - version: 4.3.4(next@15.4.1(@babel/core@7.28.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.89.2))(react@19.1.0)(typescript@5.8.3) + version: 4.3.4(next@15.4.2(@babel/core@7.28.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.89.2))(react@19.1.0)(typescript@5.8.3) react: specifier: 19.1.0 version: 19.1.0 @@ -1992,14 +2044,14 @@ importers: specifier: ^8.1.3 version: 8.1.3(react@19.1.0) '@tabler/icons-react': - specifier: ^3.34.0 - version: 3.34.0(react@19.1.0) + specifier: ^3.34.1 + version: 3.34.1(react@19.1.0) mantine-react-table: specifier: 2.0.0-beta.9 - version: 2.0.0-beta.9(@mantine/core@8.1.3(@mantine/hooks@8.1.3(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@mantine/dates@8.1.3(@mantine/core@8.1.3(@mantine/hooks@8.1.3(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@mantine/hooks@8.1.3(react@19.1.0))(dayjs@1.11.13)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@mantine/hooks@8.1.3(react@19.1.0))(@tabler/icons-react@3.34.0(react@19.1.0))(clsx@2.1.1)(dayjs@1.11.13)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + version: 2.0.0-beta.9(@mantine/core@8.1.3(@mantine/hooks@8.1.3(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@mantine/dates@8.1.3(@mantine/core@8.1.3(@mantine/hooks@8.1.3(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@mantine/hooks@8.1.3(react@19.1.0))(dayjs@1.11.13)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@mantine/hooks@8.1.3(react@19.1.0))(@tabler/icons-react@3.34.1(react@19.1.0))(clsx@2.1.1)(dayjs@1.11.13)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) next: - specifier: 15.4.1 - version: 15.4.1(@babel/core@7.28.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.89.2) + specifier: 15.4.2 + version: 15.4.2(@babel/core@7.28.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.89.2) react: specifier: 19.1.0 version: 19.1.0 @@ -2141,8 +2193,8 @@ importers: specifier: ^8.1.3 version: 8.1.3(react@19.1.0) '@tabler/icons-react': - specifier: ^3.34.0 - version: 3.34.0(react@19.1.0) + specifier: ^3.34.1 + version: 3.34.1(react@19.1.0) '@tiptap/extension-color': specifier: 2.26.1 version: 2.26.1(@tiptap/core@2.26.1(@tiptap/pm@2.26.1))(@tiptap/extension-text-style@2.26.1(@tiptap/core@2.26.1(@tiptap/pm@2.26.1))) @@ -2199,10 +2251,10 @@ importers: version: 1.11.13 mantine-react-table: specifier: 2.0.0-beta.9 - version: 2.0.0-beta.9(@mantine/core@8.1.3(@mantine/hooks@8.1.3(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@mantine/dates@8.1.3(@mantine/core@8.1.3(@mantine/hooks@8.1.3(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@mantine/hooks@8.1.3(react@19.1.0))(dayjs@1.11.13)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@mantine/hooks@8.1.3(react@19.1.0))(@tabler/icons-react@3.34.0(react@19.1.0))(clsx@2.1.1)(dayjs@1.11.13)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + version: 2.0.0-beta.9(@mantine/core@8.1.3(@mantine/hooks@8.1.3(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@mantine/dates@8.1.3(@mantine/core@8.1.3(@mantine/hooks@8.1.3(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@mantine/hooks@8.1.3(react@19.1.0))(dayjs@1.11.13)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@mantine/hooks@8.1.3(react@19.1.0))(@tabler/icons-react@3.34.1(react@19.1.0))(clsx@2.1.1)(dayjs@1.11.13)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) next: - specifier: 15.4.1 - version: 15.4.1(@babel/core@7.28.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.89.2) + specifier: 15.4.2 + version: 15.4.2(@babel/core@7.28.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.89.2) react: specifier: 19.1.0 version: 19.1.0 @@ -2244,11 +2296,11 @@ importers: tooling/eslint: dependencies: '@next/eslint-plugin-next': - specifier: 15.4.1 - version: 15.4.1 + specifier: 15.4.2 + version: 15.4.2 eslint-config-prettier: - specifier: ^10.1.7 - version: 10.1.7(eslint@9.31.0) + specifier: ^10.1.8 + version: 10.1.8(eslint@9.31.0) eslint-config-turbo: specifier: ^2.5.5 version: 2.5.5(eslint@9.31.0)(turbo@2.5.5) @@ -2296,8 +2348,8 @@ importers: specifier: workspace:^0.1.0 version: link:../typescript prettier-plugin-packagejson: - specifier: ^2.5.18 - version: 2.5.18(prettier@3.6.2) + specifier: ^2.5.19 + version: 2.5.19(prettier@3.6.2) typescript: specifier: ^5.8.3 version: 5.8.3 @@ -2657,8 +2709,8 @@ packages: cpu: [ppc64] os: [aix] - '@esbuild/aix-ppc64@0.25.6': - resolution: {integrity: sha512-ShbM/3XxwuxjFiuVBHA+d3j5dyac0aEVVq1oluIDf71hUw0aRF59dV/efUsIwFnR6m8JNM2FjZOzmaZ8yG61kw==} + '@esbuild/aix-ppc64@0.25.8': + resolution: {integrity: sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] @@ -2681,8 +2733,8 @@ packages: cpu: [arm64] os: [android] - '@esbuild/android-arm64@0.25.6': - resolution: {integrity: sha512-hd5zdUarsK6strW+3Wxi5qWws+rJhCCbMiC9QZyzoxfk5uHRIE8T287giQxzVpEvCwuJ9Qjg6bEjcRJcgfLqoA==} + '@esbuild/android-arm64@0.25.8': + resolution: {integrity: sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==} engines: {node: '>=18'} cpu: [arm64] os: [android] @@ -2705,8 +2757,8 @@ packages: cpu: [arm] os: [android] - '@esbuild/android-arm@0.25.6': - resolution: {integrity: sha512-S8ToEOVfg++AU/bHwdksHNnyLyVM+eMVAOf6yRKFitnwnbwwPNqKr3srzFRe7nzV69RQKb5DgchIX5pt3L53xg==} + '@esbuild/android-arm@0.25.8': + resolution: {integrity: sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==} engines: {node: '>=18'} cpu: [arm] os: [android] @@ -2729,8 +2781,8 @@ packages: cpu: [x64] os: [android] - '@esbuild/android-x64@0.25.6': - resolution: {integrity: sha512-0Z7KpHSr3VBIO9A/1wcT3NTy7EB4oNC4upJ5ye3R7taCc2GUdeynSLArnon5G8scPwaU866d3H4BCrE5xLW25A==} + '@esbuild/android-x64@0.25.8': + resolution: {integrity: sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==} engines: {node: '>=18'} cpu: [x64] os: [android] @@ -2753,8 +2805,8 @@ packages: cpu: [arm64] os: [darwin] - '@esbuild/darwin-arm64@0.25.6': - resolution: {integrity: sha512-FFCssz3XBavjxcFxKsGy2DYK5VSvJqa6y5HXljKzhRZ87LvEi13brPrf/wdyl/BbpbMKJNOr1Sd0jtW4Ge1pAA==} + '@esbuild/darwin-arm64@0.25.8': + resolution: {integrity: sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] @@ -2777,8 +2829,8 @@ packages: cpu: [x64] os: [darwin] - '@esbuild/darwin-x64@0.25.6': - resolution: {integrity: sha512-GfXs5kry/TkGM2vKqK2oyiLFygJRqKVhawu3+DOCk7OxLy/6jYkWXhlHwOoTb0WqGnWGAS7sooxbZowy+pK9Yg==} + '@esbuild/darwin-x64@0.25.8': + resolution: {integrity: sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==} engines: {node: '>=18'} cpu: [x64] os: [darwin] @@ -2801,8 +2853,8 @@ packages: cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-arm64@0.25.6': - resolution: {integrity: sha512-aoLF2c3OvDn2XDTRvn8hN6DRzVVpDlj2B/F66clWd/FHLiHaG3aVZjxQX2DYphA5y/evbdGvC6Us13tvyt4pWg==} + '@esbuild/freebsd-arm64@0.25.8': + resolution: {integrity: sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] @@ -2825,8 +2877,8 @@ packages: cpu: [x64] os: [freebsd] - '@esbuild/freebsd-x64@0.25.6': - resolution: {integrity: sha512-2SkqTjTSo2dYi/jzFbU9Plt1vk0+nNg8YC8rOXXea+iA3hfNJWebKYPs3xnOUf9+ZWhKAaxnQNUf2X9LOpeiMQ==} + '@esbuild/freebsd-x64@0.25.8': + resolution: {integrity: sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] @@ -2849,8 +2901,8 @@ packages: cpu: [arm64] os: [linux] - '@esbuild/linux-arm64@0.25.6': - resolution: {integrity: sha512-b967hU0gqKd9Drsh/UuAm21Khpoh6mPBSgz8mKRq4P5mVK8bpA+hQzmm/ZwGVULSNBzKdZPQBRT3+WuVavcWsQ==} + '@esbuild/linux-arm64@0.25.8': + resolution: {integrity: sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==} engines: {node: '>=18'} cpu: [arm64] os: [linux] @@ -2873,8 +2925,8 @@ packages: cpu: [arm] os: [linux] - '@esbuild/linux-arm@0.25.6': - resolution: {integrity: sha512-SZHQlzvqv4Du5PrKE2faN0qlbsaW/3QQfUUc6yO2EjFcA83xnwm91UbEEVx4ApZ9Z5oG8Bxz4qPE+HFwtVcfyw==} + '@esbuild/linux-arm@0.25.8': + resolution: {integrity: sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==} engines: {node: '>=18'} cpu: [arm] os: [linux] @@ -2897,8 +2949,8 @@ packages: cpu: [ia32] os: [linux] - '@esbuild/linux-ia32@0.25.6': - resolution: {integrity: sha512-aHWdQ2AAltRkLPOsKdi3xv0mZ8fUGPdlKEjIEhxCPm5yKEThcUjHpWB1idN74lfXGnZ5SULQSgtr5Qos5B0bPw==} + '@esbuild/linux-ia32@0.25.8': + resolution: {integrity: sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==} engines: {node: '>=18'} cpu: [ia32] os: [linux] @@ -2921,8 +2973,8 @@ packages: cpu: [loong64] os: [linux] - '@esbuild/linux-loong64@0.25.6': - resolution: {integrity: sha512-VgKCsHdXRSQ7E1+QXGdRPlQ/e08bN6WMQb27/TMfV+vPjjTImuT9PmLXupRlC90S1JeNNW5lzkAEO/McKeJ2yg==} + '@esbuild/linux-loong64@0.25.8': + resolution: {integrity: sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==} engines: {node: '>=18'} cpu: [loong64] os: [linux] @@ -2945,8 +2997,8 @@ packages: cpu: [mips64el] os: [linux] - '@esbuild/linux-mips64el@0.25.6': - resolution: {integrity: sha512-WViNlpivRKT9/py3kCmkHnn44GkGXVdXfdc4drNmRl15zVQ2+D2uFwdlGh6IuK5AAnGTo2qPB1Djppj+t78rzw==} + '@esbuild/linux-mips64el@0.25.8': + resolution: {integrity: sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] @@ -2969,8 +3021,8 @@ packages: cpu: [ppc64] os: [linux] - '@esbuild/linux-ppc64@0.25.6': - resolution: {integrity: sha512-wyYKZ9NTdmAMb5730I38lBqVu6cKl4ZfYXIs31Baf8aoOtB4xSGi3THmDYt4BTFHk7/EcVixkOV2uZfwU3Q2Jw==} + '@esbuild/linux-ppc64@0.25.8': + resolution: {integrity: sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] @@ -2993,8 +3045,8 @@ packages: cpu: [riscv64] os: [linux] - '@esbuild/linux-riscv64@0.25.6': - resolution: {integrity: sha512-KZh7bAGGcrinEj4qzilJ4hqTY3Dg2U82c8bv+e1xqNqZCrCyc+TL9AUEn5WGKDzm3CfC5RODE/qc96OcbIe33w==} + '@esbuild/linux-riscv64@0.25.8': + resolution: {integrity: sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] @@ -3017,8 +3069,8 @@ packages: cpu: [s390x] os: [linux] - '@esbuild/linux-s390x@0.25.6': - resolution: {integrity: sha512-9N1LsTwAuE9oj6lHMyyAM+ucxGiVnEqUdp4v7IaMmrwb06ZTEVCIs3oPPplVsnjPfyjmxwHxHMF8b6vzUVAUGw==} + '@esbuild/linux-s390x@0.25.8': + resolution: {integrity: sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==} engines: {node: '>=18'} cpu: [s390x] os: [linux] @@ -3041,14 +3093,14 @@ packages: cpu: [x64] os: [linux] - '@esbuild/linux-x64@0.25.6': - resolution: {integrity: sha512-A6bJB41b4lKFWRKNrWoP2LHsjVzNiaurf7wyj/XtFNTsnPuxwEBWHLty+ZE0dWBKuSK1fvKgrKaNjBS7qbFKig==} + '@esbuild/linux-x64@0.25.8': + resolution: {integrity: sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.25.6': - resolution: {integrity: sha512-IjA+DcwoVpjEvyxZddDqBY+uJ2Snc6duLpjmkXm/v4xuS3H+3FkLZlDm9ZsAbF9rsfP3zeA0/ArNDORZgrxR/Q==} + '@esbuild/netbsd-arm64@0.25.8': + resolution: {integrity: sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] @@ -3071,14 +3123,14 @@ packages: cpu: [x64] os: [netbsd] - '@esbuild/netbsd-x64@0.25.6': - resolution: {integrity: sha512-dUXuZr5WenIDlMHdMkvDc1FAu4xdWixTCRgP7RQLBOkkGgwuuzaGSYcOpW4jFxzpzL1ejb8yF620UxAqnBrR9g==} + '@esbuild/netbsd-x64@0.25.8': + resolution: {integrity: sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.25.6': - resolution: {integrity: sha512-l8ZCvXP0tbTJ3iaqdNf3pjaOSd5ex/e6/omLIQCVBLmHTlfXW3zAxQ4fnDmPLOB1x9xrcSi/xtCWFwCZRIaEwg==} + '@esbuild/openbsd-arm64@0.25.8': + resolution: {integrity: sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] @@ -3101,14 +3153,14 @@ packages: cpu: [x64] os: [openbsd] - '@esbuild/openbsd-x64@0.25.6': - resolution: {integrity: sha512-hKrmDa0aOFOr71KQ/19JC7az1P0GWtCN1t2ahYAf4O007DHZt/dW8ym5+CUdJhQ/qkZmI1HAF8KkJbEFtCL7gw==} + '@esbuild/openbsd-x64@0.25.8': + resolution: {integrity: sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/openharmony-arm64@0.25.6': - resolution: {integrity: sha512-+SqBcAWoB1fYKmpWoQP4pGtx+pUUC//RNYhFdbcSA16617cchuryuhOCRpPsjCblKukAckWsV+aQ3UKT/RMPcA==} + '@esbuild/openharmony-arm64@0.25.8': + resolution: {integrity: sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] @@ -3131,8 +3183,8 @@ packages: cpu: [x64] os: [sunos] - '@esbuild/sunos-x64@0.25.6': - resolution: {integrity: sha512-dyCGxv1/Br7MiSC42qinGL8KkG4kX0pEsdb0+TKhmJZgCUDBGmyo1/ArCjNGiOLiIAgdbWgmWgib4HoCi5t7kA==} + '@esbuild/sunos-x64@0.25.8': + resolution: {integrity: sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==} engines: {node: '>=18'} cpu: [x64] os: [sunos] @@ -3155,8 +3207,8 @@ packages: cpu: [arm64] os: [win32] - '@esbuild/win32-arm64@0.25.6': - resolution: {integrity: sha512-42QOgcZeZOvXfsCBJF5Afw73t4veOId//XD3i+/9gSkhSV6Gk3VPlWncctI+JcOyERv85FUo7RxuxGy+z8A43Q==} + '@esbuild/win32-arm64@0.25.8': + resolution: {integrity: sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==} engines: {node: '>=18'} cpu: [arm64] os: [win32] @@ -3179,8 +3231,8 @@ packages: cpu: [ia32] os: [win32] - '@esbuild/win32-ia32@0.25.6': - resolution: {integrity: sha512-4AWhgXmDuYN7rJI6ORB+uU9DHLq/erBbuMoAuB4VWJTu5KtCgcKYPynF0YI1VkBNuEfjNlLrFr9KZPJzrtLkrQ==} + '@esbuild/win32-ia32@0.25.8': + resolution: {integrity: sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==} engines: {node: '>=18'} cpu: [ia32] os: [win32] @@ -3203,8 +3255,8 @@ packages: cpu: [x64] os: [win32] - '@esbuild/win32-x64@0.25.6': - resolution: {integrity: sha512-NgJPHHbEpLQgDH2MjQu90pzW/5vvXIZ7KOnPyNBm92A6WgZ/7b6fJyUBjoumLqeOQQGqY2QjQxRo97ah4Sj0cA==} + '@esbuild/win32-x64@0.25.8': + resolution: {integrity: sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==} engines: {node: '>=18'} cpu: [x64] os: [win32] @@ -3309,16 +3361,16 @@ packages: '@formatjs/intl-localematcher@0.5.5': resolution: {integrity: sha512-t5tOGMgZ/i5+ALl2/offNqAQq/lfUnKLEw0mXQI4N4bqpedhrSE+fyKLpwnd22sK0dif6AV+ufQcTsKShB9J1g==} - '@gitbeaker/core@42.5.0': - resolution: {integrity: sha512-rMWpOPaZi1iLiifnOIoVO57p2EmQQdfIwP4txqNyMvG4WjYP5Ez0U7jRD9Nra41x6K5kTPBZkuQcAdxVWRJcEQ==} + '@gitbeaker/core@43.3.0': + resolution: {integrity: sha512-hORHoQnDFHKOuKDAL6qfFTom+eZvHNGTELuwQ22rFmqW9D3el2XQ+L/fcXEcyEysTGB49b2t2FtRf0Us5Et10g==} engines: {node: '>=18.20.0'} - '@gitbeaker/requester-utils@42.5.0': - resolution: {integrity: sha512-HLdLS9LPBMVQumvroQg/4qkphLDtwDB+ygEsrD2u4oYCMUtXV4V1xaVqU4yTXjbTJ5sItOtdB43vYRkBcgueBw==} + '@gitbeaker/requester-utils@43.3.0': + resolution: {integrity: sha512-A5bQQAi8cH6qFTiyWWiAgtdM1XV8vRD3OPWs0jOpEIEbT+rWJZQRclo3dlKhcsiM+Gz4SRAylwBwUYszdjOzdw==} engines: {node: '>=18.20.0'} - '@gitbeaker/rest@42.5.0': - resolution: {integrity: sha512-oC5cM6jS7aFOp0luTw5mWSRuMgdxwHRLZQ/aWkI+ETMfsprR/HyxsXfljlMY/XJ/fRxTbRJiodR5Axf66WjO3w==} + '@gitbeaker/rest@43.3.0': + resolution: {integrity: sha512-3plSNawPLgHSfJmbBGGzCT3kvKE5rqS4fhzTyQFfiwm/+NvOTxEIqMQ5wxaYWlonYfR/eUVp09sMig1nG9Isww==} engines: {node: '>=18.20.0'} '@grpc/grpc-js@1.12.5': @@ -3682,56 +3734,56 @@ packages: '@ndaidong/bellajs@12.0.1': resolution: {integrity: sha512-1iY42uiHz0cxNMbde7O3zVN+ZX1viOOUOBRt6ht6lkRZbSjwOnFV34Zv4URp3hGzEe6L9Byk7BOq/41H0PzAOQ==} - '@next/env@15.4.1': - resolution: {integrity: sha512-DXQwFGAE2VH+f2TJsKepRXpODPU+scf5fDbKOME8MMyeyswe4XwgRdiiIYmBfkXU+2ssliLYznajTrOQdnLR5A==} + '@next/env@15.4.2': + resolution: {integrity: sha512-kd7MvW3pAP7tmk1NaiX4yG15xb2l4gNhteKQxt3f+NGR22qwPymn9RBuv26QKfIKmfo6z2NpgU8W2RT0s0jlvg==} - '@next/eslint-plugin-next@15.4.1': - resolution: {integrity: sha512-lQnHUxN7mMksK7IxgKDIXNMWFOBmksVrjamMEURXiYfo7zgsc30lnU8u4y/MJktSh+nB80ktTQeQbWdQO6c8Ow==} + '@next/eslint-plugin-next@15.4.2': + resolution: {integrity: sha512-k0rjdWjXBY6tAOty1ckrMETE6Mx66d85NsgcAIdDp7/cXOsTJ93ywmbg3uUcpxX5TUHFEcCWI5mb8nPhwCe9jg==} - '@next/swc-darwin-arm64@15.4.1': - resolution: {integrity: sha512-L+81yMsiHq82VRXS2RVq6OgDwjvA4kDksGU8hfiDHEXP+ncKIUhUsadAVB+MRIp2FErs/5hpXR0u2eluWPAhig==} + '@next/swc-darwin-arm64@15.4.2': + resolution: {integrity: sha512-ovqjR8NjCBdBf1U+R/Gvn0RazTtXS9n6wqs84iFaCS1NHbw9ksVE4dfmsYcLoyUVd9BWE0bjkphOWrrz8uz/uw==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@15.4.1': - resolution: {integrity: sha512-jfz1RXu6SzL14lFl05/MNkcN35lTLMJWPbqt7Xaj35+ZWAX342aePIJrN6xBdGeKl6jPXJm0Yqo3Xvh3Gpo3Uw==} + '@next/swc-darwin-x64@15.4.2': + resolution: {integrity: sha512-I8d4W7tPqbdbHRI4z1iBfaoJIBrEG4fnWKIe+Rj1vIucNZ5cEinfwkBt3RcDF00bFRZRDpvKuDjgMFD3OyRBnw==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@15.4.1': - resolution: {integrity: sha512-k0tOFn3dsnkaGfs6iQz8Ms6f1CyQe4GacXF979sL8PNQxjYS1swx9VsOyUQYaPoGV8nAZ7OX8cYaeiXGq9ahPQ==} + '@next/swc-linux-arm64-gnu@15.4.2': + resolution: {integrity: sha512-lvhz02dU3Ec5thzfQ2RCUeOFADjNkS/px1W7MBt7HMhf0/amMfT8Z/aXOwEA+cVWN7HSDRSUc8hHILoHmvajsg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-arm64-musl@15.4.1': - resolution: {integrity: sha512-4ogGQ/3qDzbbK3IwV88ltihHFbQVq6Qr+uEapzXHXBH1KsVBZOB50sn6BWHPcFjwSoMX2Tj9eH/fZvQnSIgc3g==} + '@next/swc-linux-arm64-musl@15.4.2': + resolution: {integrity: sha512-v+5PPfL8UP+KKHS3Mox7QMoeFdMlaV0zeNMIF7eLC4qTiVSO0RPNnK0nkBZSD5BEkkf//c+vI9s/iHxddCZchA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-x64-gnu@15.4.1': - resolution: {integrity: sha512-Jj0Rfw3wIgp+eahMz/tOGwlcYYEFjlBPKU7NqoOkTX0LY45i5W0WcDpgiDWSLrN8KFQq/LW7fZq46gxGCiOYlQ==} + '@next/swc-linux-x64-gnu@15.4.2': + resolution: {integrity: sha512-PHLYOC9W2cu6I/JEKo77+LW4uPNvyEQiSkVRUQPsOIsf01PRr8PtPhwtz3XNnC9At8CrzPkzqQ9/kYDg4R4Inw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-linux-x64-musl@15.4.1': - resolution: {integrity: sha512-9WlEZfnw1vFqkWsTMzZDgNL7AUI1aiBHi0S2m8jvycPyCq/fbZjtE/nDkhJRYbSjXbtRHYLDBlmP95kpjEmJbw==} + '@next/swc-linux-x64-musl@15.4.2': + resolution: {integrity: sha512-lpmUF9FfLFns4JbTu+5aJGA8aR9dXaA12eoNe9CJbVkGib0FDiPa4kBGTwy0xDxKNGlv3bLDViyx1U+qafmuJQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-win32-arm64-msvc@15.4.1': - resolution: {integrity: sha512-WodRbZ9g6CQLRZsG3gtrA9w7Qfa9BwDzhFVdlI6sV0OCPq9JrOrJSp9/ioLsezbV8w9RCJ8v55uzJuJ5RgWLZg==} + '@next/swc-win32-arm64-msvc@15.4.2': + resolution: {integrity: sha512-aMjogoGnRepas0LQ/PBPsvvUzj+IoXw2IoDSEShEtrsu2toBiaxEWzOQuPZ8nie8+1iF7TA63S7rlp3YWAjNEg==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-x64-msvc@15.4.1': - resolution: {integrity: sha512-y+wTBxelk2xiNofmDOVU7O5WxTHcvOoL3srOM0kxTzKDjQ57kPU0tpnPJ/BWrRnsOwXEv0+3QSbGR7hY4n9LkQ==} + '@next/swc-win32-x64-msvc@15.4.2': + resolution: {integrity: sha512-FxwauyexSFu78wEqR/+NB9MnqXVj6SxJKwcVs2CRjeSX/jBagDCgtR2W36PZUYm0WPgY1pQ3C1+nn7zSnwROuw==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -3954,8 +4006,8 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} - '@pkgr/core@0.2.4': - resolution: {integrity: sha512-ROFF39F6ZrnzSUEmQQZUar0Jt4xVoP9WnDRdWwF4NNcXs3xBTLgBUDoOwW141y1jP+S8nahIbdxbFC7IShw9Iw==} + '@pkgr/core@0.2.9': + resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} '@pnpm/config.env-replace@1.1.0': @@ -4321,13 +4373,13 @@ packages: zod: optional: true - '@tabler/icons-react@3.34.0': - resolution: {integrity: sha512-OpEIR2iZsIXECtAIMbn1zfKfQ3zKJjXyIZlkgOGUL9UkMCFycEiF2Y8AVfEQsyre/3FnBdlWJvGr0NU47n2TbQ==} + '@tabler/icons-react@3.34.1': + resolution: {integrity: sha512-Ld6g0NqOO05kyyHsfU8h787PdHBm7cFmOycQSIrGp45XcXYDuOK2Bs0VC4T2FWSKZ6bx5g04imfzazf/nqtk1A==} peerDependencies: react: '>= 16' - '@tabler/icons@3.34.0': - resolution: {integrity: sha512-jtVqv0JC1WU2TTEBN32D9+R6mc1iEBuPwLnBsWaR02SIEciu9aq5806AWkCHuObhQ4ERhhXErLEK7Fs+tEZxiA==} + '@tabler/icons@3.34.1': + resolution: {integrity: sha512-9gTnUvd7Fd/DmQgr3MKY+oJLa1RfNsQo8c/ir3TJAWghOuZXodbtbVp0QBY2DxWuuvrSZFys0HEbv1CoiI5y6A==} '@tanstack/match-sorter-utils@8.19.4': resolution: {integrity: sha512-Wo1iKt2b9OT7d+YGhvEPD3DXvPv2etTusIMhMUoG7fbhmxcXCtIjJDEygy91Y2JFlwGyjqiBPRozme7UD8hoqg==} @@ -4377,8 +4429,11 @@ packages: '@tanstack/virtual-core@3.11.2': resolution: {integrity: sha512-vTtpNt7mKCiZ1pwU9hfKPhpdVO2sVzFQsxoVBGtOSHxlrRRzYr8iQ2TlwbAcRYCcEiZ9ECAM8kBzH0v2+VzfKw==} - '@testcontainers/mysql@11.2.1': - resolution: {integrity: sha512-grOpOcFvDBuwSGVgtIT1lOb/RcVGVfuoxsnngO/TeepZ5XXS50Wjj/HmdxG88XXAKEjVv2I9A/6T02nAV8JyLw==} + '@testcontainers/mysql@11.3.1': + resolution: {integrity: sha512-MC5AzgifmW8decbnAXrDSlU5cK2ZKwgBmIXFxVFQ34V+aR6tqf0boJlMjvVV5Th5+minRiliMd0wpw5RdGQNrg==} + + '@testcontainers/redis@11.3.1': + resolution: {integrity: sha512-fs3IDQuZa8fF9HzcY6HSWeT94Ovx84T0UhaiDuUDHS7dR/tZFlE5WBw4oGDSxsvYYu7n/O47KOMhTHfUS8x1IA==} '@tiptap/core@2.26.1': resolution: {integrity: sha512-fymyd/XZvYiHjBoLt1gxs024xP/LY26d43R1vluYq7AHBL/7DE3ywzy+1GEsGyAv5Je2L0KBhNIR/izbq3Kaqg==} @@ -6462,8 +6517,8 @@ packages: engines: {node: '>=12'} hasBin: true - esbuild@0.25.6: - resolution: {integrity: sha512-GVuzuUwtdsghE3ocJ9Bs8PNoF13HNQ5TXbEi2AhvVb8xU1Iwt9Fos9FEamfoee+u/TOsn7GUWc04lz46n2bbTg==} + esbuild@0.25.8: + resolution: {integrity: sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==} engines: {node: '>=18'} hasBin: true @@ -6492,8 +6547,8 @@ packages: engines: {node: '>=6.0'} hasBin: true - eslint-config-prettier@10.1.7: - resolution: {integrity: sha512-xztdELuHs7grBM+qdMUF4M4SjPpeOMN3kx7sGU6ifl5yibck/GRa0+0d+m1lPsGNkd+2bIWh2lUUTzX7MX/obw==} + eslint-config-prettier@10.1.8: + resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==} hasBin: true peerDependencies: eslint: '>=7.0.0' @@ -8255,8 +8310,8 @@ packages: typescript: optional: true - next@15.4.1: - resolution: {integrity: sha512-eNKB1q8C7o9zXF8+jgJs2CzSLIU3T6bQtX6DcTnCq1sIR1CJ0GlSyRs1BubQi3/JgCnr9Vr+rS5mOMI38FFyQw==} + next@15.4.2: + resolution: {integrity: sha512-oH1rmFso+84NIkocfuxaGKcXIjMUTmnzV2x0m8qsYtB4gD6iflLMESXt5XJ8cFgWMBei4v88rNr/j+peNg72XA==} engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} hasBin: true peerDependencies: @@ -8856,8 +8911,8 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} - prettier-plugin-packagejson@2.5.18: - resolution: {integrity: sha512-NKznPGcGrcj4NPGxnh+w78JXPyfB6I4RQSCM0v+CAXwpDG7OEpJQ5zMyfC5NBgKH1k7Skwcj5ak5by2mrHvC5g==} + prettier-plugin-packagejson@2.5.19: + resolution: {integrity: sha512-Qsqp4+jsZbKMpEGZB1UP1pxeAT8sCzne2IwnKkr+QhUe665EXUo3BAvTf1kAPCqyMv9kg3ZmO0+7eOni/C6Uag==} peerDependencies: prettier: '>= 1.16.0' peerDependenciesMeta: @@ -9011,6 +9066,10 @@ packages: resolution: {integrity: sha512-EJPeIn0CYrGu+hli1xilKAPXODtJ12T0sP63Ijx2/khC2JtuaN3JyNIpvmnkmaEtha9ocbG4A4cMcr+TvqvwQg==} engines: {node: '>=0.6'} + qs@6.14.0: + resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} + engines: {node: '>=0.6'} + querystringify@2.2.0: resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} @@ -9927,8 +9986,8 @@ packages: symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} - synckit@0.11.8: - resolution: {integrity: sha512-+XZ+r1XGIJGeQk3VvXhT6xx/VpbHsRzsTkGgF6E5RX9TTXD0118l87puaEBZ566FhqblC6U0d4XnubznJDm30A==} + synckit@0.11.11: + resolution: {integrity: sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==} engines: {node: ^14.18.0 || >=16.0.0} tabbable@6.2.0: @@ -9987,8 +10046,8 @@ packages: resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==} engines: {node: '>=18'} - testcontainers@11.2.1: - resolution: {integrity: sha512-KJALGi8ButKDZgzHr0PtJUVNBOSlSFncumZ34MCQTN4VEU9AK4tWTn9gCcAFzG4zBmzzC2aEbHMFUujqkbDvBg==} + testcontainers@11.3.1: + resolution: {integrity: sha512-5oO0oQjF7gdWdXCVYC3vH6iSTDW8rQsWq7p5xdR17ujhhiPfQgf0hptqmeCCcnht/Qw6wWk5h1JlZnaJPP/Bhg==} text-decoder@1.2.0: resolution: {integrity: sha512-n1yg1mOj9DNpk3NeZOx7T6jchTbyJS3i3cucbNN6FcdPriMZx7NsgrGpWWdWZZGxD7ES1XB+3uoqHMgOKaN+fg==} @@ -11379,7 +11438,7 @@ snapshots: '@esbuild/aix-ppc64@0.21.5': optional: true - '@esbuild/aix-ppc64@0.25.6': + '@esbuild/aix-ppc64@0.25.8': optional: true '@esbuild/android-arm64@0.18.20': @@ -11391,7 +11450,7 @@ snapshots: '@esbuild/android-arm64@0.21.5': optional: true - '@esbuild/android-arm64@0.25.6': + '@esbuild/android-arm64@0.25.8': optional: true '@esbuild/android-arm@0.18.20': @@ -11403,7 +11462,7 @@ snapshots: '@esbuild/android-arm@0.21.5': optional: true - '@esbuild/android-arm@0.25.6': + '@esbuild/android-arm@0.25.8': optional: true '@esbuild/android-x64@0.18.20': @@ -11415,7 +11474,7 @@ snapshots: '@esbuild/android-x64@0.21.5': optional: true - '@esbuild/android-x64@0.25.6': + '@esbuild/android-x64@0.25.8': optional: true '@esbuild/darwin-arm64@0.18.20': @@ -11427,7 +11486,7 @@ snapshots: '@esbuild/darwin-arm64@0.21.5': optional: true - '@esbuild/darwin-arm64@0.25.6': + '@esbuild/darwin-arm64@0.25.8': optional: true '@esbuild/darwin-x64@0.18.20': @@ -11439,7 +11498,7 @@ snapshots: '@esbuild/darwin-x64@0.21.5': optional: true - '@esbuild/darwin-x64@0.25.6': + '@esbuild/darwin-x64@0.25.8': optional: true '@esbuild/freebsd-arm64@0.18.20': @@ -11451,7 +11510,7 @@ snapshots: '@esbuild/freebsd-arm64@0.21.5': optional: true - '@esbuild/freebsd-arm64@0.25.6': + '@esbuild/freebsd-arm64@0.25.8': optional: true '@esbuild/freebsd-x64@0.18.20': @@ -11463,7 +11522,7 @@ snapshots: '@esbuild/freebsd-x64@0.21.5': optional: true - '@esbuild/freebsd-x64@0.25.6': + '@esbuild/freebsd-x64@0.25.8': optional: true '@esbuild/linux-arm64@0.18.20': @@ -11475,7 +11534,7 @@ snapshots: '@esbuild/linux-arm64@0.21.5': optional: true - '@esbuild/linux-arm64@0.25.6': + '@esbuild/linux-arm64@0.25.8': optional: true '@esbuild/linux-arm@0.18.20': @@ -11487,7 +11546,7 @@ snapshots: '@esbuild/linux-arm@0.21.5': optional: true - '@esbuild/linux-arm@0.25.6': + '@esbuild/linux-arm@0.25.8': optional: true '@esbuild/linux-ia32@0.18.20': @@ -11499,7 +11558,7 @@ snapshots: '@esbuild/linux-ia32@0.21.5': optional: true - '@esbuild/linux-ia32@0.25.6': + '@esbuild/linux-ia32@0.25.8': optional: true '@esbuild/linux-loong64@0.18.20': @@ -11511,7 +11570,7 @@ snapshots: '@esbuild/linux-loong64@0.21.5': optional: true - '@esbuild/linux-loong64@0.25.6': + '@esbuild/linux-loong64@0.25.8': optional: true '@esbuild/linux-mips64el@0.18.20': @@ -11523,7 +11582,7 @@ snapshots: '@esbuild/linux-mips64el@0.21.5': optional: true - '@esbuild/linux-mips64el@0.25.6': + '@esbuild/linux-mips64el@0.25.8': optional: true '@esbuild/linux-ppc64@0.18.20': @@ -11535,7 +11594,7 @@ snapshots: '@esbuild/linux-ppc64@0.21.5': optional: true - '@esbuild/linux-ppc64@0.25.6': + '@esbuild/linux-ppc64@0.25.8': optional: true '@esbuild/linux-riscv64@0.18.20': @@ -11547,7 +11606,7 @@ snapshots: '@esbuild/linux-riscv64@0.21.5': optional: true - '@esbuild/linux-riscv64@0.25.6': + '@esbuild/linux-riscv64@0.25.8': optional: true '@esbuild/linux-s390x@0.18.20': @@ -11559,7 +11618,7 @@ snapshots: '@esbuild/linux-s390x@0.21.5': optional: true - '@esbuild/linux-s390x@0.25.6': + '@esbuild/linux-s390x@0.25.8': optional: true '@esbuild/linux-x64@0.18.20': @@ -11571,10 +11630,10 @@ snapshots: '@esbuild/linux-x64@0.21.5': optional: true - '@esbuild/linux-x64@0.25.6': + '@esbuild/linux-x64@0.25.8': optional: true - '@esbuild/netbsd-arm64@0.25.6': + '@esbuild/netbsd-arm64@0.25.8': optional: true '@esbuild/netbsd-x64@0.18.20': @@ -11586,10 +11645,10 @@ snapshots: '@esbuild/netbsd-x64@0.21.5': optional: true - '@esbuild/netbsd-x64@0.25.6': + '@esbuild/netbsd-x64@0.25.8': optional: true - '@esbuild/openbsd-arm64@0.25.6': + '@esbuild/openbsd-arm64@0.25.8': optional: true '@esbuild/openbsd-x64@0.18.20': @@ -11601,10 +11660,10 @@ snapshots: '@esbuild/openbsd-x64@0.21.5': optional: true - '@esbuild/openbsd-x64@0.25.6': + '@esbuild/openbsd-x64@0.25.8': optional: true - '@esbuild/openharmony-arm64@0.25.6': + '@esbuild/openharmony-arm64@0.25.8': optional: true '@esbuild/sunos-x64@0.18.20': @@ -11616,7 +11675,7 @@ snapshots: '@esbuild/sunos-x64@0.21.5': optional: true - '@esbuild/sunos-x64@0.25.6': + '@esbuild/sunos-x64@0.25.8': optional: true '@esbuild/win32-arm64@0.18.20': @@ -11628,7 +11687,7 @@ snapshots: '@esbuild/win32-arm64@0.21.5': optional: true - '@esbuild/win32-arm64@0.25.6': + '@esbuild/win32-arm64@0.25.8': optional: true '@esbuild/win32-ia32@0.18.20': @@ -11640,7 +11699,7 @@ snapshots: '@esbuild/win32-ia32@0.21.5': optional: true - '@esbuild/win32-ia32@0.25.6': + '@esbuild/win32-ia32@0.25.8': optional: true '@esbuild/win32-x64@0.18.20': @@ -11652,7 +11711,7 @@ snapshots: '@esbuild/win32-x64@0.21.5': optional: true - '@esbuild/win32-x64@0.25.6': + '@esbuild/win32-x64@0.25.8': optional: true '@eslint-community/eslint-utils@4.7.0(eslint@9.31.0)': @@ -11785,23 +11844,23 @@ snapshots: dependencies: tslib: 2.8.1 - '@gitbeaker/core@42.5.0': + '@gitbeaker/core@43.3.0': dependencies: - '@gitbeaker/requester-utils': 42.5.0 - qs: 6.13.1 + '@gitbeaker/requester-utils': 43.3.0 + qs: 6.14.0 xcase: 2.0.1 - '@gitbeaker/requester-utils@42.5.0': + '@gitbeaker/requester-utils@43.3.0': dependencies: picomatch-browser: 2.2.6 - qs: 6.13.1 + qs: 6.14.0 rate-limiter-flexible: 4.0.1 xcase: 2.0.1 - '@gitbeaker/rest@42.5.0': + '@gitbeaker/rest@43.3.0': dependencies: - '@gitbeaker/core': 42.5.0 - '@gitbeaker/requester-utils': 42.5.0 + '@gitbeaker/core': 43.3.0 + '@gitbeaker/requester-utils': 43.3.0 '@grpc/grpc-js@1.12.5': dependencies: @@ -12203,34 +12262,34 @@ snapshots: '@ndaidong/bellajs@12.0.1': {} - '@next/env@15.4.1': {} + '@next/env@15.4.2': {} - '@next/eslint-plugin-next@15.4.1': + '@next/eslint-plugin-next@15.4.2': dependencies: fast-glob: 3.3.1 - '@next/swc-darwin-arm64@15.4.1': + '@next/swc-darwin-arm64@15.4.2': optional: true - '@next/swc-darwin-x64@15.4.1': + '@next/swc-darwin-x64@15.4.2': optional: true - '@next/swc-linux-arm64-gnu@15.4.1': + '@next/swc-linux-arm64-gnu@15.4.2': optional: true - '@next/swc-linux-arm64-musl@15.4.1': + '@next/swc-linux-arm64-musl@15.4.2': optional: true - '@next/swc-linux-x64-gnu@15.4.1': + '@next/swc-linux-x64-gnu@15.4.2': optional: true - '@next/swc-linux-x64-musl@15.4.1': + '@next/swc-linux-x64-musl@15.4.2': optional: true - '@next/swc-win32-arm64-msvc@15.4.1': + '@next/swc-win32-arm64-msvc@15.4.2': optional: true - '@next/swc-win32-x64-msvc@15.4.1': + '@next/swc-win32-x64-msvc@15.4.2': optional: true '@noble/hashes@1.5.0': {} @@ -12469,7 +12528,7 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true - '@pkgr/core@0.2.4': {} + '@pkgr/core@0.2.9': {} '@pnpm/config.env-replace@1.1.0': {} @@ -13069,12 +13128,12 @@ snapshots: typescript: 5.8.3 zod: 3.25.76 - '@tabler/icons-react@3.34.0(react@19.1.0)': + '@tabler/icons-react@3.34.1(react@19.1.0)': dependencies: - '@tabler/icons': 3.34.0 + '@tabler/icons': 3.34.1 react: 19.1.0 - '@tabler/icons@3.34.0': {} + '@tabler/icons@3.34.1': {} '@tanstack/match-sorter-utils@8.19.4': dependencies: @@ -13090,10 +13149,10 @@ snapshots: '@tanstack/react-query': 5.83.0(react@19.1.0) react: 19.1.0 - '@tanstack/react-query-next-experimental@5.83.0(@tanstack/react-query@5.83.0(react@19.1.0))(next@15.4.1(@babel/core@7.26.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.89.2))(react@19.1.0)': + '@tanstack/react-query-next-experimental@5.83.0(@tanstack/react-query@5.83.0(react@19.1.0))(next@15.4.2(@babel/core@7.26.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.89.2))(react@19.1.0)': dependencies: '@tanstack/react-query': 5.83.0(react@19.1.0) - next: 15.4.1(@babel/core@7.26.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.89.2) + next: 15.4.2(@babel/core@7.26.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.89.2) react: 19.1.0 '@tanstack/react-query@5.83.0(react@19.1.0)': @@ -13117,9 +13176,16 @@ snapshots: '@tanstack/virtual-core@3.11.2': {} - '@testcontainers/mysql@11.2.1': + '@testcontainers/mysql@11.3.1': dependencies: - testcontainers: 11.2.1 + testcontainers: 11.3.1 + transitivePeerDependencies: + - bare-buffer + - supports-color + + '@testcontainers/redis@11.3.1': + dependencies: + testcontainers: 11.3.1 transitivePeerDependencies: - bare-buffer - supports-color @@ -13351,11 +13417,11 @@ snapshots: '@trpc/server': 11.4.3(typescript@5.8.3) typescript: 5.8.3 - '@trpc/next@11.4.3(@tanstack/react-query@5.83.0(react@19.1.0))(@trpc/client@11.4.3(@trpc/server@11.4.3(typescript@5.8.3))(typescript@5.8.3))(@trpc/react-query@11.4.3(@tanstack/react-query@5.83.0(react@19.1.0))(@trpc/client@11.4.3(@trpc/server@11.4.3(typescript@5.8.3))(typescript@5.8.3))(@trpc/server@11.4.3(typescript@5.8.3))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.8.3))(@trpc/server@11.4.3(typescript@5.8.3))(next@15.4.1(@babel/core@7.26.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.89.2))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.8.3)': + '@trpc/next@11.4.3(@tanstack/react-query@5.83.0(react@19.1.0))(@trpc/client@11.4.3(@trpc/server@11.4.3(typescript@5.8.3))(typescript@5.8.3))(@trpc/react-query@11.4.3(@tanstack/react-query@5.83.0(react@19.1.0))(@trpc/client@11.4.3(@trpc/server@11.4.3(typescript@5.8.3))(typescript@5.8.3))(@trpc/server@11.4.3(typescript@5.8.3))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.8.3))(@trpc/server@11.4.3(typescript@5.8.3))(next@15.4.2(@babel/core@7.26.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.89.2))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.8.3)': dependencies: '@trpc/client': 11.4.3(@trpc/server@11.4.3(typescript@5.8.3))(typescript@5.8.3) '@trpc/server': 11.4.3(typescript@5.8.3) - next: 15.4.1(@babel/core@7.26.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.89.2) + next: 15.4.2(@babel/core@7.26.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.89.2) react: 19.1.0 react-dom: 19.1.0(react@19.1.0) typescript: 5.8.3 @@ -15154,8 +15220,8 @@ snapshots: dependencies: '@drizzle-team/brocli': 0.10.2 '@esbuild-kit/esm-loader': 2.6.5 - esbuild: 0.25.6 - esbuild-register: 3.6.0(esbuild@0.25.6) + esbuild: 0.25.8 + esbuild-register: 3.6.0(esbuild@0.25.8) transitivePeerDependencies: - supports-color @@ -15487,10 +15553,10 @@ snapshots: is-date-object: 1.1.0 is-symbol: 1.1.1 - esbuild-register@3.6.0(esbuild@0.25.6): + esbuild-register@3.6.0(esbuild@0.25.8): dependencies: debug: 4.4.1 - esbuild: 0.25.6 + esbuild: 0.25.8 transitivePeerDependencies: - supports-color @@ -15571,34 +15637,34 @@ snapshots: '@esbuild/win32-ia32': 0.21.5 '@esbuild/win32-x64': 0.21.5 - esbuild@0.25.6: + esbuild@0.25.8: optionalDependencies: - '@esbuild/aix-ppc64': 0.25.6 - '@esbuild/android-arm': 0.25.6 - '@esbuild/android-arm64': 0.25.6 - '@esbuild/android-x64': 0.25.6 - '@esbuild/darwin-arm64': 0.25.6 - '@esbuild/darwin-x64': 0.25.6 - '@esbuild/freebsd-arm64': 0.25.6 - '@esbuild/freebsd-x64': 0.25.6 - '@esbuild/linux-arm': 0.25.6 - '@esbuild/linux-arm64': 0.25.6 - '@esbuild/linux-ia32': 0.25.6 - '@esbuild/linux-loong64': 0.25.6 - '@esbuild/linux-mips64el': 0.25.6 - '@esbuild/linux-ppc64': 0.25.6 - '@esbuild/linux-riscv64': 0.25.6 - '@esbuild/linux-s390x': 0.25.6 - '@esbuild/linux-x64': 0.25.6 - '@esbuild/netbsd-arm64': 0.25.6 - '@esbuild/netbsd-x64': 0.25.6 - '@esbuild/openbsd-arm64': 0.25.6 - '@esbuild/openbsd-x64': 0.25.6 - '@esbuild/openharmony-arm64': 0.25.6 - '@esbuild/sunos-x64': 0.25.6 - '@esbuild/win32-arm64': 0.25.6 - '@esbuild/win32-ia32': 0.25.6 - '@esbuild/win32-x64': 0.25.6 + '@esbuild/aix-ppc64': 0.25.8 + '@esbuild/android-arm': 0.25.8 + '@esbuild/android-arm64': 0.25.8 + '@esbuild/android-x64': 0.25.8 + '@esbuild/darwin-arm64': 0.25.8 + '@esbuild/darwin-x64': 0.25.8 + '@esbuild/freebsd-arm64': 0.25.8 + '@esbuild/freebsd-x64': 0.25.8 + '@esbuild/linux-arm': 0.25.8 + '@esbuild/linux-arm64': 0.25.8 + '@esbuild/linux-ia32': 0.25.8 + '@esbuild/linux-loong64': 0.25.8 + '@esbuild/linux-mips64el': 0.25.8 + '@esbuild/linux-ppc64': 0.25.8 + '@esbuild/linux-riscv64': 0.25.8 + '@esbuild/linux-s390x': 0.25.8 + '@esbuild/linux-x64': 0.25.8 + '@esbuild/netbsd-arm64': 0.25.8 + '@esbuild/netbsd-x64': 0.25.8 + '@esbuild/openbsd-arm64': 0.25.8 + '@esbuild/openbsd-x64': 0.25.8 + '@esbuild/openharmony-arm64': 0.25.8 + '@esbuild/sunos-x64': 0.25.8 + '@esbuild/win32-arm64': 0.25.8 + '@esbuild/win32-ia32': 0.25.8 + '@esbuild/win32-x64': 0.25.8 escalade@3.2.0: {} @@ -15618,7 +15684,7 @@ snapshots: optionalDependencies: source-map: 0.6.1 - eslint-config-prettier@10.1.7(eslint@9.31.0): + eslint-config-prettier@10.1.8(eslint@9.31.0): dependencies: eslint: 9.31.0 @@ -16591,7 +16657,7 @@ snapshots: dependencies: '@ioredis/commands': 1.2.0 cluster-key-slot: 1.1.2 - debug: 4.4.0 + debug: 4.4.1 denque: 2.1.0 lodash.defaults: 4.2.0 lodash.isarguments: 3.1.0 @@ -17207,16 +17273,16 @@ snapshots: make-dir@4.0.0: dependencies: - semver: 7.7.1 + semver: 7.7.2 make-error@1.3.6: {} - mantine-react-table@2.0.0-beta.9(@mantine/core@8.1.3(@mantine/hooks@8.1.3(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@mantine/dates@8.1.3(@mantine/core@8.1.3(@mantine/hooks@8.1.3(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@mantine/hooks@8.1.3(react@19.1.0))(dayjs@1.11.13)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@mantine/hooks@8.1.3(react@19.1.0))(@tabler/icons-react@3.34.0(react@19.1.0))(clsx@2.1.1)(dayjs@1.11.13)(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + mantine-react-table@2.0.0-beta.9(@mantine/core@8.1.3(@mantine/hooks@8.1.3(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@mantine/dates@8.1.3(@mantine/core@8.1.3(@mantine/hooks@8.1.3(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@mantine/hooks@8.1.3(react@19.1.0))(dayjs@1.11.13)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@mantine/hooks@8.1.3(react@19.1.0))(@tabler/icons-react@3.34.1(react@19.1.0))(clsx@2.1.1)(dayjs@1.11.13)(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: '@mantine/core': 8.1.3(@mantine/hooks@8.1.3(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@mantine/dates': 8.1.3(@mantine/core@8.1.3(@mantine/hooks@8.1.3(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@mantine/hooks@8.1.3(react@19.1.0))(dayjs@1.11.13)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@mantine/hooks': 8.1.3(react@19.1.0) - '@tabler/icons-react': 3.34.0(react@19.1.0) + '@tabler/icons-react': 3.34.1(react@19.1.0) '@tanstack/match-sorter-utils': 8.19.4 '@tanstack/react-table': 8.20.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@tanstack/react-virtual': 3.11.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -17615,25 +17681,25 @@ snapshots: netmask@2.0.2: {} - next-auth@5.0.0-beta.29(next@15.4.1(@babel/core@7.28.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.89.2))(react@19.1.0): + next-auth@5.0.0-beta.29(next@15.4.2(@babel/core@7.28.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.89.2))(react@19.1.0): dependencies: '@auth/core': 0.40.0 - next: 15.4.1(@babel/core@7.28.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.89.2) + next: 15.4.2(@babel/core@7.28.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.89.2) react: 19.1.0 - next-intl@4.3.4(next@15.4.1(@babel/core@7.28.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.89.2))(react@19.1.0)(typescript@5.8.3): + next-intl@4.3.4(next@15.4.2(@babel/core@7.28.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.89.2))(react@19.1.0)(typescript@5.8.3): dependencies: '@formatjs/intl-localematcher': 0.5.5 negotiator: 1.0.0 - next: 15.4.1(@babel/core@7.28.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.89.2) + next: 15.4.2(@babel/core@7.28.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.89.2) react: 19.1.0 use-intl: 4.3.4(react@19.1.0) optionalDependencies: typescript: 5.8.3 - next@15.4.1(@babel/core@7.26.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.89.2): + next@15.4.2(@babel/core@7.26.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.89.2): dependencies: - '@next/env': 15.4.1 + '@next/env': 15.4.2 '@swc/helpers': 0.5.15 caniuse-lite: 1.0.30001703 postcss: 8.4.31 @@ -17641,23 +17707,23 @@ snapshots: react-dom: 19.1.0(react@19.1.0) styled-jsx: 5.1.6(@babel/core@7.26.0)(react@19.1.0) optionalDependencies: - '@next/swc-darwin-arm64': 15.4.1 - '@next/swc-darwin-x64': 15.4.1 - '@next/swc-linux-arm64-gnu': 15.4.1 - '@next/swc-linux-arm64-musl': 15.4.1 - '@next/swc-linux-x64-gnu': 15.4.1 - '@next/swc-linux-x64-musl': 15.4.1 - '@next/swc-win32-arm64-msvc': 15.4.1 - '@next/swc-win32-x64-msvc': 15.4.1 + '@next/swc-darwin-arm64': 15.4.2 + '@next/swc-darwin-x64': 15.4.2 + '@next/swc-linux-arm64-gnu': 15.4.2 + '@next/swc-linux-arm64-musl': 15.4.2 + '@next/swc-linux-x64-gnu': 15.4.2 + '@next/swc-linux-x64-musl': 15.4.2 + '@next/swc-win32-arm64-msvc': 15.4.2 + '@next/swc-win32-x64-msvc': 15.4.2 sass: 1.89.2 sharp: 0.34.3 transitivePeerDependencies: - '@babel/core' - babel-plugin-macros - next@15.4.1(@babel/core@7.28.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.89.2): + next@15.4.2(@babel/core@7.28.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.89.2): dependencies: - '@next/env': 15.4.1 + '@next/env': 15.4.2 '@swc/helpers': 0.5.15 caniuse-lite: 1.0.30001703 postcss: 8.4.31 @@ -17665,14 +17731,14 @@ snapshots: react-dom: 19.1.0(react@19.1.0) styled-jsx: 5.1.6(@babel/core@7.28.0)(react@19.1.0) optionalDependencies: - '@next/swc-darwin-arm64': 15.4.1 - '@next/swc-darwin-x64': 15.4.1 - '@next/swc-linux-arm64-gnu': 15.4.1 - '@next/swc-linux-arm64-musl': 15.4.1 - '@next/swc-linux-x64-gnu': 15.4.1 - '@next/swc-linux-x64-musl': 15.4.1 - '@next/swc-win32-arm64-msvc': 15.4.1 - '@next/swc-win32-x64-msvc': 15.4.1 + '@next/swc-darwin-arm64': 15.4.2 + '@next/swc-darwin-x64': 15.4.2 + '@next/swc-linux-arm64-gnu': 15.4.2 + '@next/swc-linux-arm64-musl': 15.4.2 + '@next/swc-linux-x64-gnu': 15.4.2 + '@next/swc-linux-x64-musl': 15.4.2 + '@next/swc-win32-arm64-msvc': 15.4.2 + '@next/swc-win32-x64-msvc': 15.4.2 sass: 1.89.2 sharp: 0.34.3 transitivePeerDependencies: @@ -17691,7 +17757,7 @@ snapshots: node-abi@3.67.0: dependencies: - semver: 7.7.1 + semver: 7.7.2 node-abort-controller@3.1.1: {} @@ -17769,7 +17835,7 @@ snapshots: normalize-package-data@6.0.2: dependencies: hosted-git-info: 7.0.2 - semver: 7.7.1 + semver: 7.7.2 validate-npm-package-license: 3.0.4 normalize-path@3.0.0: {} @@ -18236,10 +18302,10 @@ snapshots: prelude-ls@1.2.1: {} - prettier-plugin-packagejson@2.5.18(prettier@3.6.2): + prettier-plugin-packagejson@2.5.19(prettier@3.6.2): dependencies: sort-package-json: 3.4.0 - synckit: 0.11.8 + synckit: 0.11.11 optionalDependencies: prettier: 3.6.2 @@ -18445,6 +18511,10 @@ snapshots: dependencies: side-channel: 1.1.0 + qs@6.14.0: + dependencies: + side-channel: 1.1.0 + querystringify@2.2.0: {} queue-microtask@1.2.3: {} @@ -19061,8 +19131,7 @@ snapshots: semver@7.7.1: {} - semver@7.7.2: - optional: true + semver@7.7.2: {} sentence-case@2.1.1: dependencies: @@ -19290,7 +19359,7 @@ snapshots: detect-newline: 4.0.1 git-hooks-list: 4.1.1 is-plain-obj: 4.1.0 - semver: 7.7.1 + semver: 7.7.2 sort-object-keys: 1.1.3 tinyglobby: 0.2.14 @@ -19621,9 +19690,9 @@ snapshots: symbol-tree@3.2.4: {} - synckit@0.11.8: + synckit@0.11.11: dependencies: - '@pkgr/core': 0.2.4 + '@pkgr/core': 0.2.9 tabbable@6.2.0: {} @@ -19701,7 +19770,7 @@ snapshots: glob: 10.4.5 minimatch: 9.0.5 - testcontainers@11.2.1: + testcontainers@11.3.1: dependencies: '@balena/dockerignore': 1.0.2 '@types/dockerode': 3.3.42 @@ -19918,7 +19987,7 @@ snapshots: tsx@4.20.3: dependencies: - esbuild: 0.25.6 + esbuild: 0.25.8 get-tsconfig: 4.8.1 optionalDependencies: fsevents: 2.3.3 diff --git a/scripts/run.sh b/scripts/run.sh index 296d39815..9fc481fdd 100644 --- a/scripts/run.sh +++ b/scripts/run.sh @@ -25,8 +25,15 @@ envsubst '${HOSTNAME}' < /etc/nginx/templates/nginx.conf > /etc/nginx/nginx.conf nginx -g 'daemon off;' & NGINX_PID=$! -redis-server /app/redis.conf & -REDIS_PID=$! +if [ $REDIS_IS_EXTERNAL = "true" ]; then + echo "Using external Redis server at redis://$REDIS_HOST:$REDIS_PORT" +else + echo "Starting internal Redis server" + redis-server /app/redis.conf & + REDIS_PID=$! +fi + + node apps/tasks/tasks.cjs & TASKS_PID=$! diff --git a/tooling/eslint/package.json b/tooling/eslint/package.json index b24895d78..79a94f1af 100644 --- a/tooling/eslint/package.json +++ b/tooling/eslint/package.json @@ -17,8 +17,8 @@ }, "prettier": "@homarr/prettier-config", "dependencies": { - "@next/eslint-plugin-next": "15.4.1", - "eslint-config-prettier": "^10.1.7", + "@next/eslint-plugin-next": "15.4.2", + "eslint-config-prettier": "^10.1.8", "eslint-config-turbo": "^2.5.5", "eslint-plugin-import": "^2.32.0", "eslint-plugin-jsx-a11y": "^6.10.2", diff --git a/tooling/prettier/package.json b/tooling/prettier/package.json index 219b8a237..06573bf5f 100644 --- a/tooling/prettier/package.json +++ b/tooling/prettier/package.json @@ -15,7 +15,7 @@ }, "devDependencies": { "@homarr/tsconfig": "workspace:^0.1.0", - "prettier-plugin-packagejson": "^2.5.18", + "prettier-plugin-packagejson": "^2.5.19", "typescript": "^5.8.3" } }