Compare commits
25
Commits
No files matched your search
@@ -1,43 +1,3 @@
|
||||
## [1.63.0](https://github.com/homarr-labs/homarr/compare/v1.62.0...v1.63.0) (2026-05-29)
|
||||
|
||||
### Features
|
||||
|
||||
* **auth:** support custom token endpoint auth methods ([#5806](https://github.com/homarr-labs/homarr/issues/5806)) ([be61602](https://github.com/homarr-labs/homarr/commit/be61602576a8bec4bb7e4928d36e0854e926264d))
|
||||
* auto-open widget settings on add with all integrations pre-selected ([#5767](https://github.com/homarr-labs/homarr/issues/5767)) ([f10bb3a](https://github.com/homarr-labs/homarr/commit/f10bb3ae23fefc9779332733706aa567cc40ba48))
|
||||
* **board:** persist category collapse state in localStorage for guests ([#5714](https://github.com/homarr-labs/homarr/issues/5714)) ([66212b4](https://github.com/homarr-labs/homarr/commit/66212b4e475738e61d2ea78f3a7e72d7a610bf12)), closes [#3355](https://github.com/homarr-labs/homarr/issues/3355)
|
||||
* **manage:** unified management page layout ([#5758](https://github.com/homarr-labs/homarr/issues/5758)) ([c252b4f](https://github.com/homarr-labs/homarr/commit/c252b4f9accc4523f222354c73f687714640701f))
|
||||
* **users:** rework general settings ([#4915](https://github.com/homarr-labs/homarr/issues/4915)) ([2991777](https://github.com/homarr-labs/homarr/commit/2991777b0a0ae72522d0769a86a91250d618dbe6))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* matine ui fixes for concistency ([#5733](https://github.com/homarr-labs/homarr/issues/5733)) ([4454e3f](https://github.com/homarr-labs/homarr/commit/4454e3fb7afa624ee2f1eaec015f2725e664ac46))
|
||||
* parse Speedtest Tracker created_at as local time ([#5751](https://github.com/homarr-labs/homarr/issues/5751)) ([b19549b](https://github.com/homarr-labs/homarr/commit/b19549b601636bcbc2fadaa270a77a9fdb48b77a))
|
||||
* prevent GitHub releases check from blocking app on API failure ([#5807](https://github.com/homarr-labs/homarr/issues/5807)) ([6cc2ac5](https://github.com/homarr-labs/homarr/commit/6cc2ac56a920563c47aaaab1b33b19a9b6d5ecee))
|
||||
* **ui:** use imported theme instead of undefined createTheme ([4a67bf0](https://github.com/homarr-labs/homarr/commit/4a67bf0ef30ff8d10b06d2d3719ccab792395669))
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **boards:** parallelize board page data fetching ([#5736](https://github.com/homarr-labs/homarr/issues/5736)) ([8f03a38](https://github.com/homarr-labs/homarr/commit/8f03a38de5b9b385da7ffcc9a963be00710acb5e))
|
||||
|
||||
## [1.62.0](https://github.com/homarr-labs/homarr/compare/v1.61.1...v1.62.0) (2026-05-22)
|
||||
|
||||
### Features
|
||||
|
||||
* **seed:** add demo mode with pre-seeded user ([#5739](https://github.com/homarr-labs/homarr/issues/5739)) ([dade477](https://github.com/homarr-labs/homarr/commit/dade477b3226765eb614e66588ebee84c45cb688))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deps:** update dependency better-sqlite3 to ^12.10.0 ([#5727](https://github.com/homarr-labs/homarr/issues/5727)) ([044ad9e](https://github.com/homarr-labs/homarr/commit/044ad9edf9c1dc9b50b9591dea0b5f0cce4be01a))
|
||||
* **docker:** improve icon matching ([#5693](https://github.com/homarr-labs/homarr/issues/5693)) ([7cd30b6](https://github.com/homarr-labs/homarr/commit/7cd30b640cbf47e4678abda18f83c0924f039b1b))
|
||||
* exclude .env files from Docker build context ([#5738](https://github.com/homarr-labs/homarr/issues/5738)) ([fa73763](https://github.com/homarr-labs/homarr/commit/fa737638a812372d904207fe7af5bd7c6f53d159))
|
||||
* **tracearr:** support absolute image urls ([#5654](https://github.com/homarr-labs/homarr/issues/5654)) ([7a12721](https://github.com/homarr-labs/homarr/commit/7a1272146c597c1f9df89d74b8389f70e7cdd61a))
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* (next branch) reduce idle memory on startup ([#5637](https://github.com/homarr-labs/homarr/issues/5637)) ([5ee84f8](https://github.com/homarr-labs/homarr/commit/5ee84f8cf464c308729281cb4db572950ba85a82))
|
||||
* **boards:** remove loading overlay and show board immediately ([#5735](https://github.com/homarr-labs/homarr/issues/5735)) ([df4b57b](https://github.com/homarr-labs/homarr/commit/df4b57bd414d4402522f9210b1470d0561c37274))
|
||||
* **memory:** combine 3 node processes into one ([#5600](https://github.com/homarr-labs/homarr/issues/5600)) ([a1aa550](https://github.com/homarr-labs/homarr/commit/a1aa5505a365688b456090a882770e561479833e))
|
||||
|
||||
## [1.61.1](https://github.com/homarr-labs/homarr/compare/v1.61.0...v1.61.1) (2026-05-18)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -30,9 +30,8 @@ const nextConfig: NextConfig = {
|
||||
serverExternalPackages: ["dockerode", "isomorphic-dompurify", "jsdom"],
|
||||
experimental: {
|
||||
optimizePackageImports: ["@mantine/core", "@mantine/hooks", "@tabler/icons-react"],
|
||||
turbopackFileSystemCacheForDev: true,
|
||||
preloadEntriesOnStart: false,
|
||||
webpackMemoryOptimizations: true,
|
||||
turbopackFileSystemCacheForDev: false,
|
||||
webpackMemoryOptimizations: false,
|
||||
},
|
||||
transpilePackages: ["@homarr/ui", "@homarr/notifications", "@homarr/modals", "@homarr/spotlight", "@homarr/widgets"],
|
||||
images: {
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
const bytesToMiB = (bytes: number) => Number((bytes / (1024 * 1024)).toFixed(2));
|
||||
|
||||
const getActiveHandlesCount = () => {
|
||||
const processWithHandles = process as NodeJS.Process & {
|
||||
_getActiveHandles?: () => unknown[];
|
||||
};
|
||||
return processWithHandles._getActiveHandles?.().length ?? null;
|
||||
};
|
||||
|
||||
const getActiveRequestsCount = () => {
|
||||
const processWithRequests = process as NodeJS.Process & {
|
||||
_getActiveRequests?: () => unknown[];
|
||||
};
|
||||
return processWithRequests._getActiveRequests?.().length ?? null;
|
||||
};
|
||||
|
||||
export const getTasksRuntimeDiagnostics = () => {
|
||||
const memory = process.memoryUsage();
|
||||
const resources = process.resourceUsage();
|
||||
|
||||
return {
|
||||
rssMiB: bytesToMiB(memory.rss),
|
||||
heapUsedMiB: bytesToMiB(memory.heapUsed),
|
||||
heapTotalMiB: bytesToMiB(memory.heapTotal),
|
||||
externalMiB: bytesToMiB(memory.external),
|
||||
arrayBuffersMiB: bytesToMiB(memory.arrayBuffers),
|
||||
uptimeSeconds: Number(process.uptime().toFixed(0)),
|
||||
userCpuMicros: resources.userCPUTime,
|
||||
systemCpuMicros: resources.systemCPUTime,
|
||||
maxRssKiB: resources.maxRSS,
|
||||
activeHandles: getActiveHandlesCount(),
|
||||
activeRequests: getActiveRequestsCount(),
|
||||
};
|
||||
};
|
||||
@@ -2,11 +2,41 @@
|
||||
import "./overrides";
|
||||
|
||||
import { jobGroup } from "@homarr/cron-jobs";
|
||||
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||
|
||||
import { getTasksRuntimeDiagnostics } from "./diagnostics";
|
||||
import { onStartAsync } from "./on-start";
|
||||
|
||||
const logger = createLogger({ module: "tasksMain" });
|
||||
const RUNTIME_DIAGNOSTICS_INTERVAL_MS = 60_000;
|
||||
|
||||
const scheduleRuntimeDiagnostics = () => {
|
||||
setInterval(() => {
|
||||
logger.info("Tasks runtime diagnostics", {
|
||||
diagnostics: getTasksRuntimeDiagnostics(),
|
||||
});
|
||||
}, RUNTIME_DIAGNOSTICS_INTERVAL_MS).unref();
|
||||
};
|
||||
|
||||
void (async () => {
|
||||
logger.info("Tasks runtime bootstrap started", {
|
||||
diagnostics: getTasksRuntimeDiagnostics(),
|
||||
});
|
||||
|
||||
await onStartAsync();
|
||||
logger.info("Tasks on-start hooks completed", {
|
||||
diagnostics: getTasksRuntimeDiagnostics(),
|
||||
});
|
||||
|
||||
await jobGroup.initializeAsync();
|
||||
logger.info("Cron job group initialized in tasks runtime", {
|
||||
diagnostics: getTasksRuntimeDiagnostics(),
|
||||
});
|
||||
|
||||
await jobGroup.startAllAsync();
|
||||
logger.info("Cron job group started in tasks runtime", {
|
||||
diagnostics: getTasksRuntimeDiagnostics(),
|
||||
jobs: jobGroup.getKeys(),
|
||||
});
|
||||
scheduleRuntimeDiagnostics();
|
||||
})();
|
||||
@@ -1,5 +1,8 @@
|
||||
import { setGlobalDispatcher } from "undici";
|
||||
|
||||
import { UndiciHttpAgent } from "@homarr/core/infrastructure/http";
|
||||
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||
|
||||
const logger = createLogger({ module: "tasksOverrides" });
|
||||
setGlobalDispatcher(new UndiciHttpAgent());
|
||||
logger.info("Configured global undici dispatcher for tasks runtime");
|
||||
@@ -6,9 +6,9 @@
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=24.16.0",
|
||||
"pnpm": ">=10.33.4"
|
||||
"pnpm": ">=10.34.1"
|
||||
},
|
||||
"packageManager": "pnpm@10.33.4",
|
||||
"packageManager": "pnpm@10.34.1",
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"better-sqlite3"
|
||||
|
||||
+7
-7
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "homarr",
|
||||
"version": "1.63.0",
|
||||
"version": "1.61.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "cross-env CI=true turbo build",
|
||||
@@ -60,9 +60,9 @@
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=24.16.0",
|
||||
"pnpm": ">=10.33.4"
|
||||
"pnpm": ">=10.34.1"
|
||||
},
|
||||
"packageManager": "pnpm@10.33.4",
|
||||
"packageManager": "pnpm@10.34.1",
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"@tree-sitter-grammars/tree-sitter-yaml",
|
||||
@@ -76,14 +76,14 @@
|
||||
"tree-sitter-json"
|
||||
],
|
||||
"overrides": {
|
||||
"@babel/helpers@<7.26.10": ">=7.29.2",
|
||||
"@babel/runtime@<7.26.10": ">=7.29.2",
|
||||
"@babel/helpers@<7.26.10": ">=7.29.7",
|
||||
"@babel/runtime@<7.26.10": ">=7.29.7",
|
||||
"axios@>=1.0.0 <1.8.2": ">=1.16.1",
|
||||
"brace-expansion@>=2.0.0 <=2.0.1": ">=5.0.6",
|
||||
"brace-expansion@>=1.0.0 <=1.1.11": ">=5.0.6",
|
||||
"esbuild@<=0.24.2": ">=0.28.0",
|
||||
"form-data@>=4.0.0 <4.0.4": ">=4.0.5",
|
||||
"hono@<4.6.5": ">=4.12.22",
|
||||
"hono@<4.6.5": ">=4.12.23",
|
||||
"linkifyjs@<4.3.2": ">=4.3.3",
|
||||
"nanoid@>=4.0.0 <5.0.9": ">=5.1.11",
|
||||
"prismjs@<1.30.0": ">=1.30.0",
|
||||
@@ -93,7 +93,7 @@
|
||||
"sha.js@<=2.4.11": ">=2.4.12",
|
||||
"tar-fs@>=3.0.0 <3.0.9": ">=3.1.2",
|
||||
"tar-fs@>=2.0.0 <2.1.3": ">=3.1.2",
|
||||
"tmp@<=0.2.3": ">=0.2.5",
|
||||
"tmp@<=0.2.3": ">=0.2.7",
|
||||
"vite@>=5.0.0 <=5.4.18": ">=8.0.14"
|
||||
},
|
||||
"patchedDependencies": {
|
||||
|
||||
@@ -9,11 +9,18 @@ import {
|
||||
getAllTrustedCertificatesAsync,
|
||||
getTrustedCertificateHostnamesAsync,
|
||||
} from "@homarr/core/infrastructure/certificates";
|
||||
import { UndiciHttpAgent } from "@homarr/core/infrastructure/http";
|
||||
|
||||
import type { TrustedCertificateHostname } from "../certificates/hostnames";
|
||||
import { UndiciHttpAgent } from "./http-agent";
|
||||
import { withTimeoutAsync } from "./timeout";
|
||||
|
||||
const CERTIFICATE_DISPATCHER_TTL_MS = 5 * 60 * 1000;
|
||||
let trustedCertificateDispatcher: {
|
||||
dispatcher: UndiciHttpAgent;
|
||||
expiresAt: number;
|
||||
} | null = null;
|
||||
let creatingTrustedCertificateDispatcherPromise: Promise<UndiciHttpAgent> | null = null;
|
||||
|
||||
export const createCustomCheckServerIdentity = (
|
||||
trustedHostnames: TrustedCertificateHostname[],
|
||||
): typeof checkServerIdentity => {
|
||||
@@ -41,6 +48,36 @@ export const createCertificateAgentAsync = async (override?: {
|
||||
});
|
||||
};
|
||||
|
||||
const getSharedCertificateAgentAsync = async () => {
|
||||
const now = Date.now();
|
||||
if (trustedCertificateDispatcher && trustedCertificateDispatcher.expiresAt > now) {
|
||||
return trustedCertificateDispatcher.dispatcher;
|
||||
}
|
||||
if (creatingTrustedCertificateDispatcherPromise) {
|
||||
return await creatingTrustedCertificateDispatcherPromise;
|
||||
}
|
||||
|
||||
creatingTrustedCertificateDispatcherPromise = createCertificateAgentAsync(undefined)
|
||||
.then((dispatcher) => {
|
||||
const previousDispatcher = trustedCertificateDispatcher?.dispatcher;
|
||||
trustedCertificateDispatcher = {
|
||||
dispatcher,
|
||||
expiresAt: Date.now() + CERTIFICATE_DISPATCHER_TTL_MS,
|
||||
};
|
||||
if (previousDispatcher) {
|
||||
void previousDispatcher.close().catch(() => {
|
||||
previousDispatcher.destroy();
|
||||
});
|
||||
}
|
||||
return dispatcher;
|
||||
})
|
||||
.finally(() => {
|
||||
creatingTrustedCertificateDispatcherPromise = null;
|
||||
});
|
||||
|
||||
return await creatingTrustedCertificateDispatcherPromise;
|
||||
};
|
||||
|
||||
export const createHttpsAgentAsync = async (override?: Pick<AgentOptions, "ca" | "checkServerIdentity">) => {
|
||||
return new HttpsAgent({
|
||||
ca: await getAllTrustedCertificatesAsync(),
|
||||
@@ -63,7 +100,7 @@ export const fetchWithTrustedCertificatesAsync = async (
|
||||
url: RequestInfo,
|
||||
options?: RequestInit & { timeout?: number },
|
||||
): Promise<Response> => {
|
||||
const agent = await createCertificateAgentAsync(undefined);
|
||||
const agent = await getSharedCertificateAgentAsync();
|
||||
if (options?.timeout) {
|
||||
return await withTimeoutAsync(
|
||||
async (signal) =>
|
||||
|
||||
@@ -7,6 +7,7 @@ import { db } from "@homarr/db";
|
||||
|
||||
import type { Logger } from "./logger";
|
||||
import type { ValidateCron } from "./validation";
|
||||
import { createProcessDiagnosticsDelta, createProcessDiagnosticsSnapshot } from "./diagnostics";
|
||||
|
||||
export interface CreateCronJobCreatorOptions<TAllowedNames extends string> {
|
||||
beforeCallback?: (name: TAllowedNames) => MaybePromise<void>;
|
||||
@@ -33,19 +34,25 @@ const createCallback = <TAllowedNames extends string, TName extends TAllowedName
|
||||
const expectedMaximumDurationInMillis = options.expectedMaximumDurationInMillis ?? 2500;
|
||||
return (callback: () => MaybePromise<void>) => {
|
||||
const catchingCallbackAsync = async () => {
|
||||
const beforeExecutionSnapshot = createProcessDiagnosticsSnapshot();
|
||||
try {
|
||||
creatorOptions.logger.logDebug("The callback of cron job started", {
|
||||
name,
|
||||
diagnostics: beforeExecutionSnapshot,
|
||||
});
|
||||
const stopwatch = new Stopwatch();
|
||||
await creatorOptions.beforeCallback?.(name);
|
||||
const beforeCallbackTook = stopwatch.getElapsedInHumanWords();
|
||||
await callback();
|
||||
const afterExecutionSnapshot = createProcessDiagnosticsSnapshot();
|
||||
const callbackTook = stopwatch.getElapsedInHumanWords();
|
||||
creatorOptions.logger.logDebug("The callback of cron job succeeded", {
|
||||
name,
|
||||
beforeCallbackTook,
|
||||
callbackTook,
|
||||
diagnosticsBefore: beforeExecutionSnapshot,
|
||||
diagnosticsAfter: afterExecutionSnapshot,
|
||||
diagnosticsDelta: createProcessDiagnosticsDelta(beforeExecutionSnapshot, afterExecutionSnapshot),
|
||||
});
|
||||
|
||||
const durationInMillis = stopwatch.getElapsedInMilliseconds();
|
||||
@@ -58,11 +65,15 @@ const createCallback = <TAllowedNames extends string, TName extends TAllowedName
|
||||
}
|
||||
await creatorOptions.onCallbackSuccess?.(name);
|
||||
} catch (error) {
|
||||
const failedExecutionSnapshot = createProcessDiagnosticsSnapshot();
|
||||
creatorOptions.logger.logError(
|
||||
new ErrorWithMetadata(
|
||||
"The callback of cron job failed",
|
||||
{
|
||||
name,
|
||||
diagnosticsBefore: beforeExecutionSnapshot,
|
||||
diagnosticsAfter: failedExecutionSnapshot,
|
||||
diagnosticsDelta: createProcessDiagnosticsDelta(beforeExecutionSnapshot, failedExecutionSnapshot),
|
||||
},
|
||||
{ cause: error },
|
||||
),
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
const bytesToMiB = (bytes: number) => Number((bytes / (1024 * 1024)).toFixed(2));
|
||||
|
||||
const getActiveHandlesCount = () => {
|
||||
const processWithHandles = process as NodeJS.Process & {
|
||||
_getActiveHandles?: () => unknown[];
|
||||
};
|
||||
return processWithHandles._getActiveHandles?.().length ?? null;
|
||||
};
|
||||
|
||||
const getActiveRequestsCount = () => {
|
||||
const processWithRequests = process as NodeJS.Process & {
|
||||
_getActiveRequests?: () => unknown[];
|
||||
};
|
||||
return processWithRequests._getActiveRequests?.().length ?? null;
|
||||
};
|
||||
|
||||
export interface ProcessDiagnosticsSnapshot {
|
||||
rssMiB: number;
|
||||
heapUsedMiB: number;
|
||||
heapTotalMiB: number;
|
||||
externalMiB: number;
|
||||
arrayBuffersMiB: number;
|
||||
uptimeSeconds: number;
|
||||
userCpuMicros: number;
|
||||
systemCpuMicros: number;
|
||||
maxRssKiB: number;
|
||||
activeHandles: number | null;
|
||||
activeRequests: number | null;
|
||||
}
|
||||
|
||||
export const createProcessDiagnosticsSnapshot = (): ProcessDiagnosticsSnapshot => {
|
||||
const memory = process.memoryUsage();
|
||||
const resources = process.resourceUsage();
|
||||
|
||||
return {
|
||||
rssMiB: bytesToMiB(memory.rss),
|
||||
heapUsedMiB: bytesToMiB(memory.heapUsed),
|
||||
heapTotalMiB: bytesToMiB(memory.heapTotal),
|
||||
externalMiB: bytesToMiB(memory.external),
|
||||
arrayBuffersMiB: bytesToMiB(memory.arrayBuffers),
|
||||
uptimeSeconds: Number(process.uptime().toFixed(0)),
|
||||
userCpuMicros: resources.userCPUTime,
|
||||
systemCpuMicros: resources.systemCPUTime,
|
||||
maxRssKiB: resources.maxRSS,
|
||||
activeHandles: getActiveHandlesCount(),
|
||||
activeRequests: getActiveRequestsCount(),
|
||||
};
|
||||
};
|
||||
|
||||
export const createProcessDiagnosticsDelta = (
|
||||
before: ProcessDiagnosticsSnapshot,
|
||||
after: ProcessDiagnosticsSnapshot,
|
||||
) => {
|
||||
return {
|
||||
rssMiB: Number((after.rssMiB - before.rssMiB).toFixed(2)),
|
||||
heapUsedMiB: Number((after.heapUsedMiB - before.heapUsedMiB).toFixed(2)),
|
||||
heapTotalMiB: Number((after.heapTotalMiB - before.heapTotalMiB).toFixed(2)),
|
||||
externalMiB: Number((after.externalMiB - before.externalMiB).toFixed(2)),
|
||||
arrayBuffersMiB: Number((after.arrayBuffersMiB - before.arrayBuffersMiB).toFixed(2)),
|
||||
uptimeSeconds: Number((after.uptimeSeconds - before.uptimeSeconds).toFixed(0)),
|
||||
userCpuMicros: after.userCpuMicros - before.userCpuMicros,
|
||||
systemCpuMicros: after.systemCpuMicros - before.systemCpuMicros,
|
||||
maxRssKiB: after.maxRssKiB - before.maxRssKiB,
|
||||
activeHandles:
|
||||
before.activeHandles === null || after.activeHandles === null ? null : after.activeHandles - before.activeHandles,
|
||||
activeRequests:
|
||||
before.activeRequests === null || after.activeRequests === null
|
||||
? null
|
||||
: after.activeRequests - before.activeRequests,
|
||||
};
|
||||
};
|
||||
@@ -4,6 +4,7 @@ import { objectEntries, objectKeys } from "@homarr/common";
|
||||
import { db } from "@homarr/db";
|
||||
|
||||
import type { JobCallback } from "./creator";
|
||||
import { createProcessDiagnosticsSnapshot } from "./diagnostics";
|
||||
import type { Logger } from "./logger";
|
||||
import { jobRegistry } from "./registry";
|
||||
|
||||
@@ -38,6 +39,10 @@ export const createJobGroupCreator = <TAllowedNames extends string = string>(
|
||||
|
||||
return {
|
||||
initializeAsync: async () => {
|
||||
options.logger.logInfo("Initializing cron job group", {
|
||||
registeredJobs: [...jobRegistry.keys()],
|
||||
diagnostics: createProcessDiagnosticsSnapshot(),
|
||||
});
|
||||
const configurations = await db.query.cronJobConfigurations.findMany();
|
||||
for (const job of jobRegistry.values()) {
|
||||
const configuration = configurations.find(({ name }) => name === job.name);
|
||||
@@ -51,7 +56,15 @@ export const createJobGroupCreator = <TAllowedNames extends string = string>(
|
||||
const scheduledTask = await job.createTaskAsync();
|
||||
|
||||
tasks.set(job.name, scheduledTask);
|
||||
options.logger.logDebug("Created scheduled task for cron job", {
|
||||
name: job.name,
|
||||
diagnostics: createProcessDiagnosticsSnapshot(),
|
||||
});
|
||||
}
|
||||
options.logger.logInfo("Cron job group initialized", {
|
||||
taskCount: tasks.size,
|
||||
diagnostics: createProcessDiagnosticsSnapshot(),
|
||||
});
|
||||
},
|
||||
startAsync: async (name: keyof TJobs) => {
|
||||
const job = jobRegistry.get(name as string);
|
||||
@@ -65,6 +78,10 @@ export const createJobGroupCreator = <TAllowedNames extends string = string>(
|
||||
tasks.get(name as string)?.resume();
|
||||
},
|
||||
startAllAsync: async () => {
|
||||
options.logger.logInfo("Starting all cron jobs", {
|
||||
taskCount: tasks.size,
|
||||
diagnostics: createProcessDiagnosticsSnapshot(),
|
||||
});
|
||||
for (const job of jobRegistry.values()) {
|
||||
if (!tasks.has(job.name)) {
|
||||
continue;
|
||||
@@ -76,6 +93,10 @@ export const createJobGroupCreator = <TAllowedNames extends string = string>(
|
||||
await job.onStartAsync();
|
||||
tasks.get(job.name)?.resume();
|
||||
}
|
||||
options.logger.logInfo("Started all cron jobs", {
|
||||
taskCount: tasks.size,
|
||||
diagnostics: createProcessDiagnosticsSnapshot(),
|
||||
});
|
||||
},
|
||||
runManuallyAsync: async (name: keyof TJobs) => {
|
||||
const job = jobRegistry.get(name as string);
|
||||
|
||||
@@ -9,6 +9,7 @@ import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error";
|
||||
import { createGetSetChannel } from "@homarr/redis";
|
||||
|
||||
const logger = createLogger({ module: "imageProxy" });
|
||||
const IMAGE_PROXY_ENTRY_TTL_SECONDS = 60 * 60 * 24 * 7;
|
||||
|
||||
const createHmacChannel = (hmac: `${string}.${string}`) => createGetSetChannel<string>(`image-proxy:hmac:${hmac}`);
|
||||
const createUrlByIdChannel = (id: string) =>
|
||||
@@ -31,12 +32,17 @@ export class ImageProxy {
|
||||
public async createImageAsync(url: string, headers?: Record<string, string>): Promise<string> {
|
||||
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 existingImageData = await createUrlByIdChannel(existingId).getAsync();
|
||||
if (!existingImageData) {
|
||||
await this.removeHashForImageAsync(url, headers);
|
||||
} else {
|
||||
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();
|
||||
@@ -116,8 +122,10 @@ export class ImageProxy {
|
||||
await urlHeaderChannel.setAsync({
|
||||
url: encryptSecret(url),
|
||||
headers: encryptSecret(JSON.stringify(headers ?? null)),
|
||||
}, {
|
||||
ttlSeconds: IMAGE_PROXY_ENTRY_TTL_SECONDS,
|
||||
});
|
||||
await hashChannel.setAsync(id);
|
||||
await hashChannel.setAsync(id, { ttlSeconds: IMAGE_PROXY_ENTRY_TTL_SECONDS });
|
||||
|
||||
logger.debug("Stored image in the proxy", {
|
||||
id,
|
||||
@@ -126,6 +134,13 @@ export class ImageProxy {
|
||||
});
|
||||
}
|
||||
|
||||
private async removeHashForImageAsync(url: string, headers: Record<string, string> | undefined): Promise<void> {
|
||||
const urlHash = this.hashSecret(url);
|
||||
const headerHash = this.hashSecret(JSON.stringify(headers ?? null));
|
||||
|
||||
await createHmacChannel(`${urlHash}.${headerHash}`).removeAsync();
|
||||
}
|
||||
|
||||
private hashSecret(value: string): string {
|
||||
return createHmac("sha256", ImageProxy.getHmacKey()).update(value).digest("hex");
|
||||
}
|
||||
|
||||
@@ -113,8 +113,14 @@ export const createGetSetChannel = <TData>(name: string) => {
|
||||
* Set data in the channel
|
||||
* @param data data to be stored in the channel
|
||||
*/
|
||||
setAsync: async (data: TData) => {
|
||||
await getSetClient.set(name, superjson.stringify(data));
|
||||
setAsync: async (data: TData, options?: { ttlSeconds?: number }) => {
|
||||
const ttlSeconds = options?.ttlSeconds;
|
||||
const serializedData = superjson.stringify(data);
|
||||
if (ttlSeconds && ttlSeconds > 0) {
|
||||
await getSetClient.set(name, serializedData, "EX", Math.ceil(ttlSeconds));
|
||||
return;
|
||||
}
|
||||
await getSetClient.set(name, serializedData);
|
||||
},
|
||||
/**
|
||||
* Remove data from the channel
|
||||
|
||||
@@ -1056,7 +1056,7 @@
|
||||
}
|
||||
},
|
||||
"noResults": {
|
||||
"title": ""
|
||||
"title": "Nessun media caricato"
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
@@ -1088,8 +1088,8 @@
|
||||
"checkLogs": "Controlla i log per maggiori dettagli",
|
||||
"tryAgain": "Riprova",
|
||||
"loading": "Caricamento",
|
||||
"show": "",
|
||||
"hide": ""
|
||||
"show": "Visualizza",
|
||||
"hide": "Nascondi"
|
||||
},
|
||||
"here": "qui",
|
||||
"iconPicker": {
|
||||
@@ -1255,7 +1255,7 @@
|
||||
"label": {
|
||||
"create": "Nuova categoria",
|
||||
"changePosition": "Cambia posizione",
|
||||
"settings": ""
|
||||
"settings": "Impostazioni"
|
||||
}
|
||||
},
|
||||
"openAllInNewTabs": {
|
||||
@@ -3459,7 +3459,7 @@
|
||||
"private": "Questa board è privata"
|
||||
},
|
||||
"noResults": {
|
||||
"title": ""
|
||||
"title": "Ancora nessuna board"
|
||||
},
|
||||
"modal": {
|
||||
"createBoard": {
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
"form": {
|
||||
"name": {
|
||||
"label": "Gruppens namn",
|
||||
"description": ""
|
||||
"description": "Namnet måste matcha admin-grupp i extern tjänst"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -159,7 +159,7 @@
|
||||
"label": "Visa sökresultat i ny flik"
|
||||
},
|
||||
"ddgBangs": {
|
||||
"label": ""
|
||||
"label": "Aktivera DuckDuckGo bangs"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
@@ -252,10 +252,10 @@
|
||||
"changeDdgBangs": {
|
||||
"notification": {
|
||||
"success": {
|
||||
"message": ""
|
||||
"message": "DuckDuckGo bangs inställningen ändrad"
|
||||
},
|
||||
"error": {
|
||||
"message": ""
|
||||
"message": "DuckDuckGo bangs inställningen kunde inte ändras"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -350,12 +350,12 @@
|
||||
"description": "Tillåt medlemmar att addera valfri applikation på sina tavlor"
|
||||
},
|
||||
"modify-all": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
"label": "Modifiera alla appar",
|
||||
"description": "Tillåt att medlemmar modifierar alla appar"
|
||||
},
|
||||
"full-all": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
"label": "Fullständiga rättigheter till appar",
|
||||
"description": "Tillåt att medlemmar hanterar, använder och raderar alla appar"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -363,20 +363,20 @@
|
||||
"title": "Tavlor",
|
||||
"item": {
|
||||
"create": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
"label": "Skapa tavlor",
|
||||
"description": "Tillåt att medlemmar skapar tavlor"
|
||||
},
|
||||
"view-all": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
"label": "Se alla tavlor",
|
||||
"description": "Tillåt att medlemmar ser alla tavlor"
|
||||
},
|
||||
"modify-all": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
"label": "Modifiera alla tavlor",
|
||||
"description": "Tillåt att medlemmar modifierar alla tavlor (Detta inkluderar inte åtkomstkontroll eller farozonen)"
|
||||
},
|
||||
"full-all": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
"label": "Fullständiga rättigheter till tavlor",
|
||||
"description": "Tillåt att medlemmar ser, modifierar och tar bort alla tavlor (detta inkluderar åtkomstkontroll och farozonen)"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -392,38 +392,38 @@
|
||||
"description": "Tillåter medlemmar att addera valfri integration till sina tavlor"
|
||||
},
|
||||
"interact-all": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
"label": "Interagera med alla integrationer",
|
||||
"description": "Tillåt att medlemmar interagerar med alla integrationer"
|
||||
},
|
||||
"full-all": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
"label": "Fullständiga rättigheter till integrationer",
|
||||
"description": "Tillåt att medlemmar hanterar, använder och interagerar med alla integrationer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"media": {
|
||||
"title": "",
|
||||
"title": "Media",
|
||||
"item": {
|
||||
"upload": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
"label": "Ladda upp media",
|
||||
"description": "Tillåt att medlemmar laddar upp media"
|
||||
},
|
||||
"view-all": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
"label": "Se all media",
|
||||
"description": "Tillåt att medlemmar ser all media"
|
||||
},
|
||||
"full-all": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
"label": "Fullständiga rättigheter till media",
|
||||
"description": "Tillåt att medlemmar hanterar och raderar all media"
|
||||
}
|
||||
}
|
||||
},
|
||||
"other": {
|
||||
"title": "",
|
||||
"title": "Annat",
|
||||
"item": {
|
||||
"view-logs": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
"label": "Se loggar",
|
||||
"description": "Tillåt att medlemmar ser loggar"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -435,32 +435,32 @@
|
||||
"description": "Tillåt medlemmar att skapa sökmotorer"
|
||||
},
|
||||
"modify-all": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
"label": "Modifiera alla sökmotorer",
|
||||
"description": "Tillåt att medlemmar modifierar alla sökmotorer"
|
||||
},
|
||||
"full-all": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
"label": "Fullständiga rättigheter till sökmotorer",
|
||||
"description": "Tillåt att medlemmar hanterar och raderar alla sökmotorer"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"memberNotice": {
|
||||
"mixed": "",
|
||||
"external": ""
|
||||
"mixed": "Vissa medlemmar är från externa tjänster och kan inte hantera här",
|
||||
"external": "Alla medlemmar är från externa tjänster och kan inte hanteras här"
|
||||
},
|
||||
"reservedNotice": {
|
||||
"message": ""
|
||||
"message": "Denna grupp är reserverad för systemet så vissa åtgärder är begränsade. <checkoutDocs></checkoutDocs>"
|
||||
},
|
||||
"action": {
|
||||
"create": {
|
||||
"label": "Addera en grupp",
|
||||
"notification": {
|
||||
"success": {
|
||||
"message": ""
|
||||
"message": "Gruppen har skapats"
|
||||
},
|
||||
"error": {
|
||||
"message": ""
|
||||
"message": "Gruppen kunde inte skapas"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -470,7 +470,7 @@
|
||||
"confirm": "Är du säker på att du vill överföra ägarskapet för gruppen {name} till {username}?",
|
||||
"notification": {
|
||||
"success": {
|
||||
"message": ""
|
||||
"message": "Överfört grupp {group} till {user}"
|
||||
},
|
||||
"error": {
|
||||
"message": "Kunde inte överföra ägarskap"
|
||||
@@ -501,38 +501,38 @@
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "Behörigheter sparade",
|
||||
"message": ""
|
||||
"message": "Rättigheterna sparade"
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
"title": "Rättigheterna ej sparade",
|
||||
"message": "Rättigheterna har inte sparats"
|
||||
}
|
||||
}
|
||||
},
|
||||
"update": {
|
||||
"notification": {
|
||||
"success": {
|
||||
"message": ""
|
||||
"message": "Gruppen {name} har sparats"
|
||||
},
|
||||
"error": {
|
||||
"message": ""
|
||||
"message": "Kunde inte spara gruppen {name}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"select": {
|
||||
"label": "Välj en grupp",
|
||||
"notFound": ""
|
||||
"notFound": "Ingen grupp hittad"
|
||||
},
|
||||
"settings": {
|
||||
"board": {
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "Inställningarna sparades",
|
||||
"message": ""
|
||||
"message": "Inställningarna för tavlan har sparats"
|
||||
},
|
||||
"error": {
|
||||
"title": "Kunde inte spara inställningarna",
|
||||
"message": ""
|
||||
"message": "Kunde inte spara inställningarna för tavlan"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -540,10 +540,10 @@
|
||||
"changePosition": {
|
||||
"notification": {
|
||||
"success": {
|
||||
"message": ""
|
||||
"message": "Ordning ändrad"
|
||||
},
|
||||
"error": {
|
||||
"message": ""
|
||||
"message": "Ordningen kunde ändras"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -559,20 +559,20 @@
|
||||
"list": {
|
||||
"title": "Applikationer",
|
||||
"noResults": {
|
||||
"title": "",
|
||||
"action": ""
|
||||
"title": "Det finns inga appar ännu",
|
||||
"action": "Skapa din första app"
|
||||
}
|
||||
},
|
||||
"create": {
|
||||
"title": "Addera applikation",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
"title": "Skapandet lyckades",
|
||||
"message": "Appen skapades framgångsrikt"
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
"title": "Skapandet misslyckades",
|
||||
"message": "Appen kunde inte skapas"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -581,11 +581,11 @@
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "Ändringarna har sparats",
|
||||
"message": ""
|
||||
"message": "Appen sparades"
|
||||
},
|
||||
"error": {
|
||||
"title": "Misslyckades med att spara ändringarna",
|
||||
"message": ""
|
||||
"message": "Appen kunde inte sparas"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -594,12 +594,12 @@
|
||||
"message": "Är du säker på att du vill ta bort applikationen {name}?",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
"title": "Borttagning lyckades",
|
||||
"message": "Appen har tagits bort"
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
"title": "Borttagning misslyckades",
|
||||
"message": "Kunde inte ta bort appen"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -612,21 +612,21 @@
|
||||
"label": "Beskrivning"
|
||||
},
|
||||
"url": {
|
||||
"label": ""
|
||||
"label": "URL"
|
||||
},
|
||||
"useDifferentUrlForPing": {
|
||||
"checkbox": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
"label": "Använd annan URL för ping",
|
||||
"description": "Användbart om Homarr kan komma åt appen direkt via ett internt host-namn eller nätverk för att undvika bandbreddsanvändning hos internetleverantör"
|
||||
}
|
||||
}
|
||||
},
|
||||
"action": {
|
||||
"select": {
|
||||
"label": "",
|
||||
"notFound": "",
|
||||
"label": "Välj app",
|
||||
"notFound": "Ingen app hittades",
|
||||
"search": "Sök efter en applikation",
|
||||
"noResults": "",
|
||||
"noResults": "Inga resultat",
|
||||
"action": "Välj {app}",
|
||||
"title": "Välj en applikation att lägga till på tavlan"
|
||||
},
|
||||
@@ -642,56 +642,56 @@
|
||||
"page": {
|
||||
"list": {
|
||||
"title": "Integrationer",
|
||||
"search": "",
|
||||
"search": "Sök integrationer",
|
||||
"noResults": {
|
||||
"title": ""
|
||||
"title": "Det finns inga integrationer ännu"
|
||||
}
|
||||
},
|
||||
"create": {
|
||||
"title": "",
|
||||
"title": "Ny {name} integration",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
"title": "Skapandet lyckades",
|
||||
"message": "Integrationen har skapats"
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
"title": "Skapandet misslyckades",
|
||||
"message": "Integrationen kunde inte skapas"
|
||||
}
|
||||
},
|
||||
"app": {
|
||||
"option": {
|
||||
"existing": {
|
||||
"title": "",
|
||||
"label": ""
|
||||
"title": "Befintlig",
|
||||
"label": "Välj befintlig app"
|
||||
},
|
||||
"new": {
|
||||
"title": "",
|
||||
"title": "Ny",
|
||||
"url": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
"label": "App URL",
|
||||
"description": "URL:en som appen kommer att öppna när den används från tavlan"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"edit": {
|
||||
"title": "",
|
||||
"title": "Redigera {name} integration",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "Ändringarna har sparats",
|
||||
"message": ""
|
||||
"message": "Integrationen sparad"
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
"title": "Kunde inte tillämpa ändringar",
|
||||
"message": "Integrationen kunde inte sparas"
|
||||
}
|
||||
},
|
||||
"app": {
|
||||
"action": {
|
||||
"add": "",
|
||||
"remove": "",
|
||||
"select": ""
|
||||
"add": "Länka en app",
|
||||
"remove": "Ta bort länk",
|
||||
"select": "Välj en app att länka till"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -700,12 +700,12 @@
|
||||
"message": "Är du säker på att du vill ta bort integrationen {name}?",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
"title": "Borttagning lyckades",
|
||||
"message": "Integrationen har tagits bort"
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
"title": "Borttagning misslyckades",
|
||||
"message": "Kunde inte ta bort integrationen"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -715,44 +715,44 @@
|
||||
"label": "Namn"
|
||||
},
|
||||
"url": {
|
||||
"label": ""
|
||||
"label": "URL"
|
||||
},
|
||||
"attemptSearchEngineCreation": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
"label": "Skapa sökmotor",
|
||||
"description": "Integrationen \"{kind}\" kan inte användas med sökmotorer. Markera detta för att automatiskt konfigurera sökmotorn."
|
||||
},
|
||||
"app": {
|
||||
"sectionTitle": ""
|
||||
"sectionTitle": "Länkad app"
|
||||
},
|
||||
"createApp": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
"label": "Skapa app",
|
||||
"description": "Skapa en app med samma namn och ikon som integrationen. Lämna fältet nedan tomt för att skapa en app med integrationens URL."
|
||||
},
|
||||
"appHref": {
|
||||
"placeholder": ""
|
||||
"placeholder": "Egen app-URL"
|
||||
}
|
||||
},
|
||||
"action": {
|
||||
"create": ""
|
||||
"create": "Ny integration"
|
||||
},
|
||||
"testConnection": {
|
||||
"action": {
|
||||
"create": "",
|
||||
"edit": ""
|
||||
"create": "Testa anslutningen och skapa",
|
||||
"edit": "Testa anslutningen och skapa"
|
||||
},
|
||||
"error": {
|
||||
"common": {
|
||||
"cause": {
|
||||
"title": ""
|
||||
"title": "Orsak med mer detaljer"
|
||||
}
|
||||
},
|
||||
"unknown": {
|
||||
"title": "",
|
||||
"description": ""
|
||||
"title": "Okänt fel",
|
||||
"description": "Ett okänt fel inträffade, öppna orsaken nedan för att se mer detaljer"
|
||||
},
|
||||
"parse": {
|
||||
"title": "",
|
||||
"description": ""
|
||||
"title": "Parsningsfel",
|
||||
"description": "Svaret kunde inte analyseras. Verifiera att URL pekar på bas-URL för tjänsten."
|
||||
},
|
||||
"authorization": {
|
||||
"title": "",
|
||||
|
||||
Generated
+474
-498
File diff suppressed because it is too large.
Load diff
+29
-29
@@ -6,7 +6,7 @@ catalog:
|
||||
"@auth/core": ^0.41.2
|
||||
"@auth/drizzle-adapter": ^1.11.2
|
||||
"@ctrl/deluge": ^8.0.0
|
||||
"@ctrl/qbittorrent": ^9.12.1
|
||||
"@ctrl/qbittorrent": ^9.12.2
|
||||
"@ctrl/transmission": ^7.4.0
|
||||
"@dnd-kit/core": ^6.3.1
|
||||
"@dnd-kit/modifiers": ^9.0.0
|
||||
@@ -20,17 +20,17 @@ catalog:
|
||||
"@immich/sdk": ^2.7.5
|
||||
"@jellyfin/sdk": ^0.13.0
|
||||
"@kubernetes/client-node": ^1.4.0
|
||||
"@mantine/charts": ^9.2.1
|
||||
"@mantine/colors-generator": ^9.2.1
|
||||
"@mantine/core": ^9.2.1
|
||||
"@mantine/dates": ^9.2.1
|
||||
"@mantine/dropzone": ^9.2.1
|
||||
"@mantine/form": ^9.2.1
|
||||
"@mantine/hooks": ^9.2.1
|
||||
"@mantine/modals": ^9.2.1
|
||||
"@mantine/notifications": ^9.2.1
|
||||
"@mantine/spotlight": ^9.2.1
|
||||
"@mantine/tiptap": ^9.2.1
|
||||
"@mantine/charts": ^9.2.2
|
||||
"@mantine/colors-generator": ^9.2.2
|
||||
"@mantine/core": ^9.2.2
|
||||
"@mantine/dates": ^9.2.2
|
||||
"@mantine/dropzone": ^9.2.2
|
||||
"@mantine/form": ^9.2.2
|
||||
"@mantine/hooks": ^9.2.2
|
||||
"@mantine/modals": ^9.2.2
|
||||
"@mantine/notifications": ^9.2.2
|
||||
"@mantine/spotlight": ^9.2.2
|
||||
"@mantine/tiptap": ^9.2.2
|
||||
"@next/eslint-plugin-next": 16.2.6
|
||||
"@octokit/auth-app": ^8.2.0
|
||||
"@paralleldrive/cuid2": ^3.3.0
|
||||
@@ -43,12 +43,12 @@ catalog:
|
||||
"@semantic-release/release-notes-generator": ^14.1.1
|
||||
"@t3-oss/env-nextjs": ^0.13.11
|
||||
"@tabler/icons-react": ^3.44.0
|
||||
"@tanstack/react-query": ^5.100.13
|
||||
"@tanstack/react-query-devtools": ^5.100.13
|
||||
"@tanstack/react-query-next-experimental": ^5.100.13
|
||||
"@testcontainers/mysql": ^12.0.0
|
||||
"@testcontainers/postgresql": ^12.0.0
|
||||
"@testcontainers/redis": ^12.0.0
|
||||
"@tanstack/react-query": ^5.100.14
|
||||
"@tanstack/react-query-devtools": ^5.100.14
|
||||
"@tanstack/react-query-next-experimental": ^5.100.14
|
||||
"@testcontainers/mysql": ^12.0.1
|
||||
"@testcontainers/postgresql": ^12.0.1
|
||||
"@testcontainers/redis": ^12.0.1
|
||||
"@tiptap/extension-color": 3.23.6
|
||||
"@tiptap/extension-highlight": 3.23.6
|
||||
"@tiptap/extension-image": 3.23.6
|
||||
@@ -70,7 +70,7 @@ catalog:
|
||||
"@trpc/react-query": ^11.17.0
|
||||
"@trpc/server": ^11.17.0
|
||||
"@trpc/tanstack-react-query": ^11.17.0
|
||||
"@turbo/gen": ^2.9.14
|
||||
"@turbo/gen": ^2.9.16
|
||||
"@types/adm-zip": 0.5.8
|
||||
"@types/bcrypt": 6.0.0
|
||||
"@types/better-sqlite3": 7.6.13
|
||||
@@ -109,7 +109,7 @@ catalog:
|
||||
croner: 10.0.1
|
||||
cross-env: ^10.1.0
|
||||
crypto-js: ^4.2.0
|
||||
dayjs: ^1.11.20
|
||||
dayjs: ^1.11.21
|
||||
deepmerge: 4.3.1
|
||||
dns-caching: ^0.2.9
|
||||
dockerode: ^5.0.0
|
||||
@@ -124,8 +124,8 @@ catalog:
|
||||
flag-icons: ^7.5.0
|
||||
glob: ^13.0.6
|
||||
ical.js: ^2.2.1
|
||||
ioredis: 5.10.1
|
||||
isomorphic-dompurify: ^3.14.0
|
||||
ioredis: 5.11.0
|
||||
isomorphic-dompurify: ^3.15.0
|
||||
jotai: ^2.20.0
|
||||
jsdom: ^29.1.1
|
||||
json5: ^2.2.3
|
||||
@@ -133,21 +133,21 @@ catalog:
|
||||
lodash.clonedeep: ^4.5.0
|
||||
mantine-react-table: 2.0.0-beta.9
|
||||
maria2: ^0.4.1
|
||||
mysql2: 3.22.3
|
||||
mysql2: 3.22.4
|
||||
next: 16.2.6
|
||||
next-auth: 5.0.0-beta.31
|
||||
next-intl: 4.12.0
|
||||
next-intl: 4.13.0
|
||||
node-loader: ^2.1.0
|
||||
octokit: ^5.0.5
|
||||
oxfmt: ^0.51.0
|
||||
oxlint: ^1.66.0
|
||||
oxfmt: ^0.52.0
|
||||
oxlint: ^1.67.0
|
||||
pg: ^8.21.0
|
||||
postcss-preset-mantine: ^1.18.0
|
||||
prismjs: ^1.30.0
|
||||
proxmox-api: 1.1.1
|
||||
react: 19.2.6
|
||||
react-dom: 19.2.6
|
||||
react-error-boundary: ^6.1.1
|
||||
react-error-boundary: ^6.1.2
|
||||
react-markdown: ^10.1.0
|
||||
react-quill-new: ^3.8.3
|
||||
react-simple-code-editor: ^0.14.1
|
||||
@@ -158,12 +158,12 @@ catalog:
|
||||
superjson: 2.2.6
|
||||
svgson: ^5.3.1
|
||||
swagger-ui-react: ^5.32.6
|
||||
testcontainers: ^12.0.0
|
||||
testcontainers: ^12.0.1
|
||||
timezones-ical-library: ^2.2.0
|
||||
trpc-to-openapi: ^3.3.0
|
||||
tsdav: ^2.2.2
|
||||
tsx: 4.20.4
|
||||
turbo: ^2.9.14
|
||||
turbo: ^2.9.16
|
||||
typescript: ^5.9.3
|
||||
undici: 7.24.6
|
||||
use-deep-compare-effect: ^1.8.1
|
||||
|
||||
Reference in new issue
Block a user