;
+}
+
+type TourController = OnboardingTourController;
+
+const stepIndexFromController = (controller: TourController) => controller.currentStepIndex ?? 0;
+
+const stepAtOffset = (controller: TourController, offset: number) => {
+ const index = stepIndexFromController(controller) + offset;
+ return controller.tour[index];
+};
+
+const pollForElement = (targetId: string, callback: () => void) => {
+ const interval = setInterval(() => {
+ const targetElement = document.querySelector(`[data-tour-target="${targetId}"]`);
+ if (targetElement) {
+ clearInterval(interval);
+ callback();
+ }
+ }, 50);
+ const timeout = setTimeout(() => clearInterval(interval), 5000);
+ return () => {
+ clearInterval(interval);
+ clearTimeout(timeout);
+ };
+};
+
+const isTypingTarget = (target: EventTarget | null) => {
+ if (!(target instanceof HTMLElement)) return false;
+ const tag = target.tagName;
+ return tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT" || target.isContentEditable;
+};
+
+interface TourForwardButtonProps {
+ label: string;
+ onClick: () => void;
+}
+
+const TourForwardButton = ({ label, onClick }: TourForwardButtonProps) => {
+ return (
+
+ );
+};
+
+interface TourDoneButtonProps {
+ label: string;
+ onClick: () => void;
+}
+
+const TourDoneButton = ({ label, onClick }: TourDoneButtonProps) => {
+ return (
+
+ );
+};
+
+export const TourShell = ({ steps, started, onEnd, stepRoutes, position, children }: TourShellProps) => {
+ const t = useI18n();
+ const router = useRouter();
+ const forwardActionRef = useRef<(() => void) | null>(null);
+
+ const navigateAndAdvance = useCallback(
+ (currentStepId: string | undefined, targetStepId: string | undefined, advance: () => void) => {
+ if (!stepRoutes || !targetStepId) {
+ advance();
+ return;
+ }
+
+ const currentRoute = currentStepId ? stepRoutes[currentStepId] : undefined;
+ const targetRoute = stepRoutes[targetStepId];
+
+ if (targetRoute && targetRoute !== currentRoute) {
+ router.push(targetRoute);
+ pollForElement(targetStepId, advance);
+ } else {
+ advance();
+ }
+ },
+ [router, stepRoutes],
+ );
+
+ const bindForwardAction = useCallback((controller: TourController, action: () => void) => {
+ forwardActionRef.current = action;
+ return action;
+ }, []);
+
+ useEffect(() => {
+ if (!started) {
+ forwardActionRef.current = null;
+ return;
+ }
+
+ const onKeyDown = (event: KeyboardEvent) => {
+ if (event.key !== "Enter" || event.shiftKey || event.ctrlKey || event.metaKey || event.altKey) return;
+ if (isTypingTarget(event.target)) return;
+ event.preventDefault();
+ forwardActionRef.current?.();
+ };
+
+ window.addEventListener("keydown", onKeyDown);
+ return () => window.removeEventListener("keydown", onKeyDown);
+ }, [started]);
+
+ return (
+ (
+ <>
+
+
+ {stepIndexFromController(controller) + 1}
+
+
+ /
+
+
+ {controller.tour.length}
+
+
+ {stepIndexFromController(controller) === 0 && (
+
+
+
+ )}
+ >
+ )}
+ nextStepNavigation={(controller) => {
+ const nextStep = stepAtOffset(controller, 1);
+ const action = bindForwardAction(controller, () => {
+ navigateAndAdvance(controller.currentStep?.id, nextStep?.id, () => controller.nextStep());
+ });
+ return ;
+ }}
+ endStepNavigation={(controller) => {
+ const action = bindForwardAction(controller, () => controller.endTour());
+ return ;
+ }}
+ prevStepNavigation={(controller) => (
+
+ )}
+ skipNavigation={(controller) => (
+
+ )}
+ withPrevButton
+ focusRevealProps={{
+ disableTargetInteraction: true,
+ popoverProps: {
+ position: position ?? { base: "bottom", sm: "right" },
+ width: 420,
+ shadow: "xl",
+ radius: "lg",
+ middlewares: { shift: { padding: 16 }, flip: true },
+ },
+ }}
+ cutoutPadding={12}
+ cutoutRadius={12}
+ >
+ {children}
+
+ );
+};
diff --git a/apps/nextjs/src/components/onboarding/tour-step-content.tsx b/apps/nextjs/src/components/onboarding/tour-step-content.tsx
new file mode 100644
index 000000000..05b7b4bd3
--- /dev/null
+++ b/apps/nextjs/src/components/onboarding/tour-step-content.tsx
@@ -0,0 +1,39 @@
+"use client";
+
+import type { ReactNode } from "react";
+import { Anchor, Divider, Group, Stack, Text, ThemeIcon } from "@mantine/core";
+import { IconExternalLink } from "@tabler/icons-react";
+
+import { useI18n } from "@homarr/translation/client";
+
+interface TourStepContentProps {
+ description: string;
+ documentationHref: string;
+ icon?: ReactNode;
+}
+
+export const TourStepContent = ({ description, documentationHref, icon }: TourStepContentProps) => {
+ const t = useI18n();
+
+ return (
+
+
+ {icon && (
+
+ {icon}
+
+ )}
+
+ {description}
+
+
+
+
+
+
+ {t("onboardingTour.learnMore")}
+
+
+
+ );
+};
diff --git a/apps/nextjs/src/components/user-avatar-menu.tsx b/apps/nextjs/src/components/user-avatar-menu.tsx
index 8faa8f6fa..71ec2b218 100644
--- a/apps/nextjs/src/components/user-avatar-menu.tsx
+++ b/apps/nextjs/src/components/user-avatar-menu.tsx
@@ -45,7 +45,7 @@ export const UserAvatarMenu = ({ children, availableUpdatesPromise }: UserAvatar
window.location.assign(logoutUrl);
return;
}
- router.refresh();
+ router.push("/auth/login");
},
});
}, [logoutUrl, openModal, router]);
diff --git a/apps/nextjs/src/env.ts b/apps/nextjs/src/env.ts
index 62b49dee8..96f03e7b7 100644
--- a/apps/nextjs/src/env.ts
+++ b/apps/nextjs/src/env.ts
@@ -3,6 +3,7 @@ import { createBooleanSchema, createEnv } from "@homarr/core/infrastructure/env"
export const env = createEnv({
server: {
UNSAFE_ENABLE_MOCK_INTEGRATION: createBooleanSchema(false),
+ DEMO_MODE: createBooleanSchema(false),
},
experimental__runtimeEnv: process.env,
});
diff --git a/deployments/prebuilt-debian/package.json b/deployments/prebuilt-debian/package.json
index ed451a14e..bc90d7d22 100644
--- a/deployments/prebuilt-debian/package.json
+++ b/deployments/prebuilt-debian/package.json
@@ -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"
diff --git a/docs/README.md b/docs/README.md
index e7aeef585..42330bec6 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -19,6 +19,9 @@
+
+ Live Demo 🎮
+ •
Install 💻
•
@@ -246,13 +249,20 @@
+
+
+
+ PeaNUT
+
+ |
+
Pi-hole
- |
-|
+ |
+
@@ -293,15 +303,15 @@
Radarr
- |
-
+ |
+
Readarr
- |
-|
+ |
+
@@ -342,15 +352,15 @@
Speedtest Tracker
- |
-
+ |
+
Tdarr
- |
-|
+ |
+
diff --git a/e2e/onboarding.spec.ts b/e2e/onboarding.spec.ts
index 7e709f425..a8a09773e 100644
--- a/e2e/onboarding.spec.ts
+++ b/e2e/onboarding.spec.ts
@@ -31,6 +31,7 @@ describe("Onboarding", () => {
confirmPassword: "Comp(exP4sswOrd",
});
await actions.processSettingsStepAsync();
+ await actions.processIntegrationsStepAsync();
// Assert
await assertions.assertFinishStepVisibleAsync();
@@ -72,6 +73,7 @@ describe("Onboarding", () => {
name: externalGroupName,
});
await actions.processSettingsStepAsync();
+ await actions.processIntegrationsStepAsync();
// Assert
await assertions.assertFinishStepVisibleAsync();
diff --git a/e2e/shared/actions/onboarding-actions.ts b/e2e/shared/actions/onboarding-actions.ts
index 6bdb1d2a5..56c09aba7 100644
--- a/e2e/shared/actions/onboarding-actions.ts
+++ b/e2e/shared/actions/onboarding-actions.ts
@@ -51,4 +51,9 @@ export class OnboardingActions {
await this.page.waitForSelector("text=Analytics");
await this.page.locator("css=button[type='submit']").click();
}
+
+ public async processIntegrationsStepAsync() {
+ await this.page.waitForSelector("text=integrations you'd like to connect");
+ await this.page.locator("button", { hasText: "Skip for now" }).click();
+ }
}
diff --git a/e2e/shared/assertions/onboarding-assertions.ts b/e2e/shared/assertions/onboarding-assertions.ts
index 63d18ea7c..12cda67d0 100644
--- a/e2e/shared/assertions/onboarding-assertions.ts
+++ b/e2e/shared/assertions/onboarding-assertions.ts
@@ -3,7 +3,7 @@ import { eq } from "drizzle-orm";
import { expect } from "vitest";
import * as sqliteSchema from "../../../packages/db/schema/sqlite";
-import { OnboardingStep } from "../../../packages/definitions/src";
+import type { OnboardingStep } from "../../../packages/definitions/src";
import { credentialsAdminGroup } from "../../../packages/definitions/src/group";
import type { SqliteDatabase } from "../e2e-db";
@@ -57,6 +57,6 @@ export class OnboardingAssertions {
}
public async assertFinishStepVisibleAsync() {
- await this.page.waitForSelector("text=completed the setup", { timeout: 5000 });
+ await this.page.waitForSelector("text=completed the setup", { timeout: 15000 });
}
}
diff --git a/package.json b/package.json
index 89d000401..5c4e75b8d 100644
--- a/package.json
+++ b/package.json
@@ -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": {
diff --git a/packages/api/src/env.ts b/packages/api/src/env.ts
index 7cc81cc57..ba26a264a 100644
--- a/packages/api/src/env.ts
+++ b/packages/api/src/env.ts
@@ -1,12 +1,14 @@
import { z } from "zod/v4";
-import { createEnv } from "@homarr/core/infrastructure/env";
+import { createBooleanSchema, createEnv } from "@homarr/core/infrastructure/env";
export const env = createEnv({
server: {
KUBERNETES_SERVICE_ACCOUNT_NAME: z.string().optional(),
+ DEMO_MODE: createBooleanSchema(false),
},
runtimeEnv: {
KUBERNETES_SERVICE_ACCOUNT_NAME: process.env.KUBERNETES_SERVICE_ACCOUNT_NAME,
+ DEMO_MODE: process.env.DEMO_MODE,
},
});
diff --git a/packages/api/src/middlewares/docker.ts b/packages/api/src/middlewares/docker.ts
index 5a317b95a..e4f04f192 100644
--- a/packages/api/src/middlewares/docker.ts
+++ b/packages/api/src/middlewares/docker.ts
@@ -2,11 +2,11 @@ import { TRPCError } from "@trpc/server";
import { env } from "@homarr/docker/env";
-import { publicProcedure } from "../trpc";
+import { isDemoMode, publicProcedure } from "../trpc";
export const dockerMiddleware = () => {
return publicProcedure.use(async ({ next }) => {
- if (env.ENABLE_DOCKER) {
+ if (env.ENABLE_DOCKER || isDemoMode) {
return await next();
}
throw new TRPCError({
diff --git a/packages/api/src/router/docker/docker-router.ts b/packages/api/src/router/docker/docker-router.ts
index 4527482f0..884998749 100644
--- a/packages/api/src/router/docker/docker-router.ts
+++ b/packages/api/src/router/docker/docker-router.ts
@@ -4,7 +4,11 @@ import { z } from "zod/v4";
import type { Container, ContainerState, Docker, Port } from "@homarr/docker";
import { DockerSingleton } from "@homarr/docker";
-import { dockerContainersRequestHandler } from "@homarr/request-handler/docker";
+import {
+ dockerContainersRequestHandler,
+ getContainerLogsAsync,
+ streamContainerLogsAsync,
+} from "@homarr/request-handler/docker";
import { dockerMiddleware } from "../../middlewares/docker";
import { createTRPCRouter, permissionRequiredProcedure } from "../../trpc";
@@ -104,6 +108,84 @@ export const dockerRouter = createTRPCRouter({
const innerHandler = dockerContainersRequestHandler.handler({});
await innerHandler.invalidateAsync();
}),
+ logs: permissionRequiredProcedure
+ .requiresPermission("admin")
+ .concat(dockerMiddleware())
+ .input(
+ z.object({
+ id: z.string(),
+ tail: z.number().min(1).max(1000).optional(),
+ }),
+ )
+ .query(async ({ input }) => {
+ const logs = await getContainerLogsAsync(input.id, input.tail ?? 200);
+
+ if (!logs) {
+ throw new TRPCError({
+ code: "NOT_FOUND",
+ message: "Container not found",
+ });
+ }
+
+ return {
+ logs,
+ };
+ }),
+ subscribeLogs: permissionRequiredProcedure
+ .requiresPermission("admin")
+ .concat(dockerMiddleware())
+ .input(
+ z.object({
+ id: z.string(),
+ tail: z.number().min(1).max(1000).optional(),
+ }),
+ )
+ .subscription(({ input }) => {
+ return observable((emit) => {
+ let cleanupFn: (() => void) | undefined;
+ let isSubscribed = true;
+
+ const initializeStreamAsync = async () => {
+ try {
+ cleanupFn = await streamContainerLogsAsync(
+ input.id,
+ input.tail ?? 200,
+ (data) => {
+ if (isSubscribed) {
+ emit.next(data);
+ }
+ },
+ (err) => {
+ if (isSubscribed) {
+ emit.error(
+ new TRPCError({
+ code: "INTERNAL_SERVER_ERROR",
+ message: err.message,
+ }),
+ );
+ }
+ },
+ );
+ } catch (err) {
+ if (isSubscribed) {
+ emit.error(
+ new TRPCError({
+ code: "NOT_FOUND",
+ message: err instanceof Error ? err.message : "Container not found",
+ }),
+ );
+ }
+ }
+ };
+
+ void initializeStreamAsync();
+
+ return () => {
+ isSubscribed = false;
+ cleanupFn?.();
+ };
+ });
+ }),
});
const getContainerOrDefaultAsync = async (instance: Docker, id: string) => {
diff --git a/packages/api/src/router/info.ts b/packages/api/src/router/info.ts
index 3c205963c..8befca412 100644
--- a/packages/api/src/router/info.ts
+++ b/packages/api/src/router/info.ts
@@ -1,7 +1,7 @@
import z from "zod/v4";
import packageJson from "../../../../package.json";
-import { createTRPCRouter, protectedProcedure } from "../trpc";
+import { createTRPCRouter, isDemoMode, publicProcedure, protectedProcedure } from "../trpc";
export const infoRouter = createTRPCRouter({
getInfo: protectedProcedure
@@ -13,4 +13,5 @@ export const infoRouter = createTRPCRouter({
version: packageJson.version,
};
}),
+ isDemoMode: publicProcedure.query(() => isDemoMode),
});
diff --git a/packages/api/src/router/integration/integration-router.ts b/packages/api/src/router/integration/integration-router.ts
index 5591b74ed..4375644cc 100644
--- a/packages/api/src/router/integration/integration-router.ts
+++ b/packages/api/src/router/integration/integration-router.ts
@@ -1,6 +1,8 @@
import { TRPCError } from "@trpc/server";
import { z } from "zod/v4";
+import { hasQueryAccessToIntegrationsAsync } from "@homarr/auth/server";
+import { constructIntegrationPermissions } from "@homarr/auth/shared";
import { createId, objectEntries } from "@homarr/common";
import { decryptSecret, encryptSecret } from "@homarr/common/server";
import { createLogger } from "@homarr/core/infrastructure/logs";
@@ -14,11 +16,9 @@ import {
integrations,
integrationSecrets,
integrationUserPermissions,
- searchEngines,
} from "@homarr/db/schema";
import type { IntegrationSecretKind } from "@homarr/definitions";
import {
- getIconUrl,
getIntegrationKindsByCategory,
getPermissionsWithParents,
integrationCategories,
@@ -33,6 +33,7 @@ import {
integrationSavePermissionsSchema,
integrationUpdateSchema,
} from "@homarr/validation/integration";
+import { mediaRequestOptionsSchema, mediaRequestRequestSchema } from "@homarr/validation/widgets/media-request";
import { createOneIntegrationMiddleware } from "../../middlewares/integration";
import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure, publicProcedure } from "../../trpc";
@@ -41,6 +42,7 @@ import { MissingSecretError, testConnectionAsync } from "./integration-test-conn
import { mapTestConnectionError } from "./map-test-connection-error";
const logger = createLogger({ module: "integrationRouter" });
+const mediaRequestSearchKinds = getIntegrationKindsByCategory("mediaSearch");
export const integrationRouter = createTRPCRouter({
all: protectedProcedure.query(async ({ ctx }) => {
@@ -80,7 +82,7 @@ export const integrationRouter = createTRPCRouter({
},
};
})
- .sort(
+ .toSorted(
(integrationA, integrationB) =>
integrationKinds.indexOf(integrationA.kind) - integrationKinds.indexOf(integrationB.kind),
);
@@ -128,7 +130,7 @@ export const integrationRouter = createTRPCRouter({
},
};
})
- .sort(
+ .toSorted(
(integrationA, integrationB) =>
integrationKinds.indexOf(integrationA.kind) - integrationKinds.indexOf(integrationB.kind),
);
@@ -179,7 +181,7 @@ export const integrationRouter = createTRPCRouter({
},
};
})
- .sort(
+ .toSorted(
(integrationA, integrationB) =>
integrationKinds.indexOf(integrationA.kind) - integrationKinds.indexOf(integrationB.kind),
);
@@ -314,20 +316,7 @@ export const integrationRouter = createTRPCRouter({
url: input.url,
});
- if (
- input.attemptSearchEngineCreation &&
- integrationDefs[input.kind].category.flatMap((category) => category).includes("search")
- ) {
- const icon = getIconUrl(input.kind);
- await ctx.db.insert(searchEngines).values({
- id: createId(),
- name: input.name,
- integrationId,
- type: "fromIntegration",
- iconUrl: icon,
- short: await getNextValidShortNameForSearchEngineAsync(ctx.db, input.name),
- });
- }
+ // Media request search is a first-class Spotlight action. Do not create a search engine for it.
}),
update: protectedProcedure.input(integrationUpdateSchema).mutation(async ({ ctx, input }) => {
await throwIfActionForbiddenAsync(ctx, eq(integrations.id, input.id), "full");
@@ -495,7 +484,7 @@ export const integrationRouter = createTRPCRouter({
});
return {
- inherited: dbGroupPermissions.sort((permissionA, permissionB) => {
+ inherited: dbGroupPermissions.toSorted((permissionA, permissionB) => {
return permissionA.group.name.localeCompare(permissionB.group.name);
}),
users: userPermissions
@@ -503,7 +492,7 @@ export const integrationRouter = createTRPCRouter({
user,
permission,
}))
- .sort((permissionA, permissionB) => {
+ .toSorted((permissionA, permissionB) => {
return (permissionA.user.name ?? "").localeCompare(permissionB.user.name ?? "");
}),
groups: dbGroupIntegrationPermission
@@ -514,7 +503,7 @@ export const integrationRouter = createTRPCRouter({
},
permission,
}))
- .sort((permissionA, permissionB) => {
+ .toSorted((permissionA, permissionB) => {
return permissionA.group.name.localeCompare(permissionB.group.name);
}),
};
@@ -618,8 +607,149 @@ export const integrationRouter = createTRPCRouter({
const integrationInstance = await createIntegrationAsync(ctx.integration);
return await integrationInstance.searchAsync(encodeURI(input.query));
}),
+ mediaRequestSearchTargets: protectedProcedure.query(async ({ ctx }) => {
+ const integrationsWithAccess = await getAccessibleMediaRequestSearchIntegrationsAsync(ctx);
+
+ return integrationsWithAccess.map((integration) => {
+ const permissions = constructIntegrationPermissions(integration, ctx.session);
+
+ return {
+ id: integration.id,
+ name: integration.name,
+ kind: integration.kind,
+ url: integration.url,
+ permissions: {
+ hasInteractAccess: permissions.hasInteractAccess,
+ },
+ };
+ });
+ }),
+ searchMediaRequests: protectedProcedure
+ .input(
+ z.object({
+ query: z.string(),
+ integrationIds: z.array(z.string()).optional(),
+ }),
+ )
+ .query(async ({ ctx, input }) => {
+ const query = input.query.trim();
+ if (query.length === 0) return [];
+
+ const integrationsWithAccess = await getAccessibleMediaRequestSearchIntegrationsAsync(ctx, input.integrationIds);
+
+ const results = await Promise.all(
+ integrationsWithAccess.map(async (integration) => {
+ const integrationInstance = await createIntegrationAsync({
+ id: integration.id,
+ name: integration.name,
+ url: integration.url,
+ kind: integration.kind as (typeof mediaRequestSearchKinds)[number],
+ externalUrl: integration.app?.href ?? null,
+ decryptedSecrets: integration.secrets.map((secret) => ({
+ ...secret,
+ value: decryptSecret(secret.value),
+ })),
+ });
+ const permissions = constructIntegrationPermissions(integration, ctx.session);
+
+ return await integrationInstance.searchAsync(encodeURI(query)).then((searchResults) =>
+ searchResults.map((result) => ({
+ ...result,
+ integration: {
+ id: integration.id,
+ name: integration.name,
+ kind: integration.kind,
+ url: integration.url,
+ permissions: {
+ hasInteractAccess: permissions.hasInteractAccess,
+ },
+ },
+ })),
+ );
+ }),
+ );
+
+ return results.flat();
+ }),
+ getMediaRequestOptions: protectedProcedure
+ .concat(createOneIntegrationMiddleware("query", "jellyseerr", "overseerr", "seerr"))
+ .input(mediaRequestOptionsSchema)
+ .query(async ({ ctx, input }) => {
+ const integration = await createIntegrationAsync(ctx.integration);
+ return await integration.getSeriesInformationAsync(input.mediaType, input.mediaId);
+ }),
+ requestMedia: protectedProcedure
+ .concat(createOneIntegrationMiddleware("interact", "jellyseerr", "overseerr", "seerr"))
+ .input(mediaRequestRequestSchema)
+ .mutation(async ({ ctx, input }) => {
+ const integration = await createIntegrationAsync(ctx.integration);
+ return await integration.requestMediaAsync(input.mediaType, input.mediaId, input.seasons);
+ }),
});
+interface IntegrationRouterContext {
+ db: Database;
+ session: Parameters[2];
+}
+
+const getAccessibleMediaRequestSearchIntegrationsAsync = async (
+ ctx: IntegrationRouterContext,
+ integrationIds?: string[],
+) => {
+ const groupsOfCurrentUser = await ctx.db.query.groupMembers.findMany({
+ where: eq(groupMembers.userId, ctx.session?.user.id ?? ""),
+ });
+
+ const integrationsFromDb = await ctx.db.query.integrations.findMany({
+ where:
+ integrationIds && integrationIds.length > 0
+ ? and(inArray(integrations.id, integrationIds), inArray(integrations.kind, mediaRequestSearchKinds))
+ : inArray(integrations.kind, mediaRequestSearchKinds),
+ orderBy: asc(integrations.name),
+ with: {
+ app: true,
+ secrets: true,
+ items: {
+ with: {
+ item: true,
+ },
+ },
+ userPermissions: {
+ where: eq(integrationUserPermissions.userId, ctx.session?.user.id ?? ""),
+ },
+ groupPermissions: {
+ where: inArray(
+ integrationGroupPermissions.groupId,
+ groupsOfCurrentUser.map((group) => group.groupId),
+ ),
+ },
+ },
+ });
+
+ if (integrationIds && integrationsFromDb.length !== integrationIds.length) {
+ throw new TRPCError({
+ code: "NOT_FOUND",
+ message: "One or more media request search integrations were not found",
+ });
+ }
+
+ const integrationsWithAccess = await Promise.all(
+ integrationsFromDb.map(async (integration) => ({
+ integration,
+ hasAccess: await hasQueryAccessToIntegrationsAsync(ctx.db, [integration], ctx.session),
+ })),
+ );
+
+ if (integrationIds && integrationsWithAccess.some(({ hasAccess }) => !hasAccess)) {
+ throw new TRPCError({
+ code: "FORBIDDEN",
+ message: "User does not have permission to query one or more media request search integrations",
+ });
+ }
+
+ return integrationsWithAccess.filter(({ hasAccess }) => hasAccess).map(({ integration }) => integration);
+};
+
interface UpdateSecretInput {
integrationId: string;
value: string;
@@ -640,35 +770,6 @@ interface AddSecretInput {
kind: IntegrationSecretKind;
}
-const getNextValidShortNameForSearchEngineAsync = async (db: Database, integrationName: string) => {
- const searchEngines = await db.query.searchEngines.findMany({
- columns: {
- short: true,
- },
- });
-
- const usedShortNames = searchEngines.flatMap((searchEngine) => searchEngine.short.toLowerCase());
- const nameByIntegrationName = integrationName.slice(0, 1).toLowerCase();
-
- if (!usedShortNames.includes(nameByIntegrationName)) {
- return nameByIntegrationName;
- }
-
- // 8 is max length constraint
- for (let i = 2; i < 9999999; i++) {
- const generatedName = `${nameByIntegrationName}${i}`;
- if (usedShortNames.includes(generatedName)) {
- continue;
- }
-
- return generatedName;
- }
-
- throw new Error(
- "Unable to automatically generate a short name. All possible variations were exhausted. Please disable the automatic creation and choose one later yourself.",
- );
-};
-
const addSecretAsync = async (db: Database, input: AddSecretInput) => {
await db.insert(integrationSecrets).values({
kind: input.kind,
diff --git a/packages/api/src/router/onboard/onboard-queries.ts b/packages/api/src/router/onboard/onboard-queries.ts
index 15f205d8b..242d861e4 100644
--- a/packages/api/src/router/onboard/onboard-queries.ts
+++ b/packages/api/src/router/onboard/onboard-queries.ts
@@ -76,6 +76,9 @@ const nextSteps: Partial {
+ const port = ports?.[0];
+ if (port?.PublicPort) {
+ const ip = port.IP && port.IP !== "0.0.0.0" && port.IP !== "::" ? port.IP : host;
+ return { url: `http://${ip}:${port.PublicPort}`, publishedPort: port.PublicPort };
+ }
+ return { url: "", publishedPort: null };
+};
+
export const onboardRouter = createTRPCRouter({
currentStep: publicProcedure.query(async ({ ctx }) => {
return await getOnboardingOrFallbackAsync(ctx.db);
@@ -33,4 +78,249 @@ export const onboardRouter = createTRPCRouter({
step: "start",
});
}),
+ createIntegration: onboardingProcedure
+ .requiresStep("integrations")
+ .input(
+ z.object({
+ name: z.string().nonempty().max(127),
+ url: z.string().nonempty(),
+ kind: zodEnumFromArray(integrationKinds),
+ secrets: z.array(
+ z.object({
+ kind: zodEnumFromArray(integrationSecretKinds),
+ value: z.string().nonempty(),
+ }),
+ ),
+ }),
+ )
+ .mutation(async ({ ctx, input }) => {
+ const result = await testConnectionAsync({
+ id: "new",
+ name: input.name,
+ url: input.url,
+ kind: input.kind,
+ secrets: input.secrets,
+ }).catch((error) => {
+ if (!(error instanceof MissingSecretError)) throw error;
+
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: error.message,
+ });
+ });
+
+ if (!result.success) {
+ return { error: mapTestConnectionError(result.error) };
+ }
+
+ const integrationId = createId();
+ const appId = createId();
+
+ await ctx.db.insert(apps).values({
+ id: appId,
+ name: input.name,
+ iconUrl: getIconUrl(input.kind),
+ href: input.url,
+ pingUrl: input.url,
+ description: null,
+ });
+
+ await ctx.db.insert(integrations).values({
+ id: integrationId,
+ name: input.name,
+ url: input.url,
+ kind: input.kind,
+ appId,
+ });
+
+ if (input.secrets.length >= 1) {
+ await ctx.db.insert(integrationSecrets).values(
+ input.secrets.map((secret) => ({
+ kind: secret.kind,
+ value: encryptSecret(secret.value),
+ integrationId,
+ })),
+ );
+ }
+ }),
+ discoverDockerServices: onboardingProcedure.requiresStep("integrations").query(async ({ ctx }) => {
+ const emptyResult = {
+ integrations: [] as {
+ containerId: string;
+ containerName: string;
+ kind: IntegrationKind;
+ suggestedUrl: string;
+ publishedPort: number | null;
+ iconUrl: string | null;
+ }[],
+ apps: [] as {
+ containerId: string;
+ containerName: string;
+ suggestedUrl: string;
+ publishedPort: number | null;
+ iconUrl: string | null;
+ }[],
+ };
+
+ try {
+ const { DockerSingleton, dockerLabels } = await import("@homarr/docker");
+
+ const dockerInstances = DockerSingleton.getInstances();
+ if (dockerInstances.length === 0) {
+ return { status: "empty" as const, ...emptyResult };
+ }
+
+ const results = await Promise.allSettled(
+ dockerInstances.map(async ({ instance, host }) => {
+ const containerList = await instance.listContainers({ all: false });
+ return containerList.filter((c) => !(dockerLabels.hide in c.Labels)).map((c) => ({ ...c, host }));
+ }),
+ );
+
+ const containers = results.flatMap((r) => (r.status === "fulfilled" ? r.value : []));
+
+ if (containers.length === 0) {
+ return { status: "empty" as const, ...emptyResult };
+ }
+
+ const likeQueries = containers.map((c) => like(icons.name, `%${extractContainerImageName(c.Image)}%`));
+ const dbIcons = likeQueries.length > 0 ? await ctx.db.query.icons.findMany({ where: or(...likeQueries) }) : [];
+
+ const seenKinds = new Set();
+ const discoveredIntegrations = emptyResult.integrations;
+ const discoveredApps = emptyResult.apps;
+
+ const cdnIconUrl = (slug: string) =>
+ `https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/${slug}.svg`;
+
+ const strictIconMatch = (imageName: string) => {
+ const normalized = imageName.toLowerCase().trim();
+ if (normalized.length < 3) return null;
+ const exact = dbIcons.find((icon) => icon.name.toLowerCase() === normalized);
+ if (exact) return exact.url;
+ return dbIcons.find((icon) => icon.name.toLowerCase().startsWith(normalized))?.url ?? null;
+ };
+
+ for (const container of containers) {
+ const imageName = extractContainerImageName(container.Image);
+ const containerName = container.Names[0]?.split("/")[1] ?? "Unknown";
+ const dbIcon = strictIconMatch(imageName);
+ const iconUrl = dbIcon ?? cdnIconUrl(imageName.toLowerCase());
+
+ const { url: suggestedUrl, publishedPort } = buildSuggestedUrl(container.Ports, container.host);
+
+ const kind = matchIntegrationKindFromContainer({
+ image: container.Image,
+ name: containerName,
+ });
+
+ if (kind && !seenKinds.has(kind)) {
+ seenKinds.add(kind);
+ discoveredIntegrations.push({
+ containerId: container.Id,
+ containerName,
+ kind,
+ suggestedUrl,
+ publishedPort,
+ iconUrl,
+ });
+ } else if (!kind && dbIcon) {
+ discoveredApps.push({
+ containerId: container.Id,
+ containerName,
+ suggestedUrl,
+ publishedPort,
+ iconUrl,
+ });
+ }
+ }
+
+ return {
+ status: "success" as const,
+ integrations: discoveredIntegrations,
+ apps: discoveredApps,
+ };
+ } catch {
+ return { status: "unavailable" as const, ...emptyResult };
+ }
+ }),
+ createAppsFromDiscovery: onboardingProcedure
+ .requiresStep("integrations")
+ .input(
+ z.array(
+ z.object({
+ name: z.string().nonempty().max(127),
+ href: z.string().nullable(),
+ iconUrl: z.string().nullable(),
+ }),
+ ),
+ )
+ .mutation(async ({ ctx, input }) => {
+ if (input.length === 0) return;
+ const defaultIcon = "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/homarr.svg";
+ await ctx.db.insert(apps).values(
+ input.map((app) => ({
+ id: createId(),
+ name: app.name,
+ iconUrl: app.iconUrl ?? defaultIcon,
+ href: app.href,
+ description: null,
+ })),
+ );
+ }),
+ setupIntegrations: onboardingProcedure.requiresStep("integrations").mutation(async ({ ctx }) => {
+ const db = ctx.db;
+
+ const allIntegrations = await db.query.integrations.findMany();
+ const allApps = await db.query.apps.findMany();
+
+ const board = await db.query.boards.findFirst({
+ with: {
+ sections: true,
+ layouts: true,
+ },
+ });
+
+ if (!board) {
+ await nextOnboardingStepAsync(db, undefined);
+ return;
+ }
+
+ const everyoneGroupRow = await db.query.groups.findFirst({
+ where: eq(groups.name, everyoneGroup),
+ });
+ if (everyoneGroupRow && !everyoneGroupRow.homeBoardId) {
+ await db.update(groups).set({ homeBoardId: board.id }).where(eq(groups.id, everyoneGroupRow.id));
+ }
+
+ const section = board.sections.find((boardSection) => boardSection.kind === "empty");
+ const layout = board.layouts[0];
+
+ if (section && layout) {
+ const existingItems = await db.query.items.findMany({
+ where: eq(items.boardId, board.id),
+ });
+ for (const item of existingItems) {
+ await db.delete(integrationItems).where(eq(integrationItems.itemId, item.id));
+ await db.delete(itemLayouts).where(eq(itemLayouts.itemId, item.id));
+ }
+ if (existingItems.length > 0) {
+ await db.delete(items).where(eq(items.boardId, board.id));
+ }
+
+ await placeAllWidgetsAsync(
+ db,
+ {
+ boardId: board.id,
+ sectionId: section.id,
+ layoutId: layout.id,
+ columnCount: layout.columnCount,
+ },
+ allIntegrations,
+ allApps,
+ );
+ }
+
+ await nextOnboardingStepAsync(db, undefined);
+ }),
});
diff --git a/packages/api/src/router/search-engine/search-engine-router.ts b/packages/api/src/router/search-engine/search-engine-router.ts
index ddfc892a9..8c72baf6e 100644
--- a/packages/api/src/router/search-engine/search-engine-router.ts
+++ b/packages/api/src/router/search-engine/search-engine-router.ts
@@ -6,12 +6,9 @@ import { createLogger } from "@homarr/core/infrastructure/logs";
import { asc, eq, like } from "@homarr/db";
import { getServerSettingByKeyAsync, updateServerSettingByKeyAsync } from "@homarr/db/queries";
import { searchEngines, users } from "@homarr/db/schema";
-import { createIntegrationAsync } from "@homarr/integrations";
import { byIdSchema, paginatedSchema, searchSchema } from "@homarr/validation/common";
import { searchEngineEditSchema, searchEngineManageSchema } from "@homarr/validation/search-engine";
-import { mediaRequestOptionsSchema, mediaRequestRequestSchema } from "@homarr/validation/widgets/media-request";
-import { createOneIntegrationMiddleware } from "../../middlewares/integration";
import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure, publicProcedure } from "../../trpc";
const logger = createLogger({ module: "searchEngineRouter" });
@@ -148,20 +145,6 @@ export const searchEngineRouter = createTRPCRouter({
limit: input.limit,
});
}),
- getMediaRequestOptions: protectedProcedure
- .concat(createOneIntegrationMiddleware("query", "jellyseerr", "overseerr"))
- .input(mediaRequestOptionsSchema)
- .query(async ({ ctx, input }) => {
- const integration = await createIntegrationAsync(ctx.integration);
- return await integration.getSeriesInformationAsync(input.mediaType, input.mediaId);
- }),
- requestMedia: protectedProcedure
- .concat(createOneIntegrationMiddleware("interact", "jellyseerr", "overseerr"))
- .input(mediaRequestRequestSchema)
- .mutation(async ({ ctx, input }) => {
- const integration = await createIntegrationAsync(ctx.integration);
- return await integration.requestMediaAsync(input.mediaType, input.mediaId, input.seasons);
- }),
create: permissionRequiredProcedure
.requiresPermission("search-engine-create")
.input(searchEngineManageSchema)
diff --git a/packages/api/src/router/test/docker/docker-router.spec.ts b/packages/api/src/router/test/docker/docker-router.spec.ts
index 941e696e6..cbea4063b 100644
--- a/packages/api/src/router/test/docker/docker-router.spec.ts
+++ b/packages/api/src/router/test/docker/docker-router.spec.ts
@@ -13,6 +13,14 @@ import { dockerRouter } from "../../docker/docker-router";
// Mock the auth module to return an empty session
vi.mock("@homarr/auth", () => ({ auth: () => ({}) as Session }));
vi.mock("@homarr/request-handler/docker", () => ({
+ getContainerLogsAsync: async () => {
+ await Promise.resolve();
+ return "logs";
+ },
+ streamContainerLogsAsync: async () => {
+ await Promise.resolve();
+ return () => undefined;
+ },
dockerContainersRequestHandler: {
handler: () => ({
getCachedOrUpdatedDataAsync: async () => {
@@ -65,6 +73,8 @@ const validInputs: {
restartAll: { ids: ["1"] },
removeAll: { ids: ["1"] },
invalidate: undefined,
+ logs: { id: "1", tail: 200 },
+ subscribeLogs: { id: "1", tail: 200 },
};
describe("All procedures should only be accessible for users with admin permission", () => {
diff --git a/packages/api/src/router/test/integration/integration-router.spec.ts b/packages/api/src/router/test/integration/integration-router.spec.ts
index 17927b9c4..a7cdddfa2 100644
--- a/packages/api/src/router/test/integration/integration-router.spec.ts
+++ b/packages/api/src/router/test/integration/integration-router.spec.ts
@@ -4,7 +4,7 @@ import { describe, expect, test, vi } from "vitest";
import type { Session } from "@homarr/auth";
import { createId } from "@homarr/common";
import { encryptSecret } from "@homarr/common/server";
-import { apps, integrations, integrationSecrets } from "@homarr/db/schema";
+import { apps, integrations, integrationSecrets, integrationUserPermissions, users } from "@homarr/db/schema";
import { createDb } from "@homarr/db/test";
import type { GroupPermissionKey } from "@homarr/definitions";
@@ -59,6 +59,127 @@ describe("all should return all integrations", () => {
});
});
+describe("mediaRequestSearchTargets should return accessible media search integrations", () => {
+ test("with integration use access should only return media request search integrations", async () => {
+ const db = createDb();
+ const caller = integrationRouter.createCaller({
+ db,
+ deviceType: undefined,
+ session: defaultSessionWithPermissions(["integration-use-all"]),
+ });
+
+ await db.insert(integrations).values([
+ {
+ id: "1",
+ name: "Jellyseerr",
+ kind: "jellyseerr",
+ url: "http://jellyseerr.local",
+ },
+ {
+ id: "2",
+ name: "Jellyfin",
+ kind: "jellyfin",
+ url: "http://jellyfin.local",
+ },
+ ]);
+
+ const result = await caller.mediaRequestSearchTargets();
+
+ expect(result).toHaveLength(1);
+ expect(result[0]!.id).toBe("1");
+ expect(result[0]!.kind).toBe("jellyseerr");
+ });
+
+ test("without access should not return media request search integrations", async () => {
+ const db = createDb();
+ const caller = integrationRouter.createCaller({
+ db,
+ deviceType: undefined,
+ session: defaultSessionWithPermissions(),
+ });
+
+ await db.insert(integrations).values({
+ id: "1",
+ name: "Jellyseerr",
+ kind: "jellyseerr",
+ url: "http://jellyseerr.local",
+ });
+
+ const result = await caller.mediaRequestSearchTargets();
+
+ expect(result).toHaveLength(0);
+ });
+
+ test("with direct integration use access should return media request search integrations", async () => {
+ const db = createDb();
+ const caller = integrationRouter.createCaller({
+ db,
+ deviceType: undefined,
+ session: defaultSessionWithPermissions(),
+ });
+
+ await db.insert(integrations).values({
+ id: "1",
+ name: "Jellyseerr",
+ kind: "jellyseerr",
+ url: "http://jellyseerr.local",
+ });
+ await db.insert(users).values({ id: defaultUserId });
+ await db.insert(integrationUserPermissions).values({
+ integrationId: "1",
+ userId: defaultUserId,
+ permission: "use",
+ });
+
+ const result = await caller.mediaRequestSearchTargets();
+
+ expect(result).toHaveLength(1);
+ expect(result[0]!.id).toBe("1");
+ });
+});
+
+describe("searchMediaRequests should validate scoped integrations", () => {
+ test("with a non media search integration should throw not found", async () => {
+ const db = createDb();
+ const caller = integrationRouter.createCaller({
+ db,
+ deviceType: undefined,
+ session: defaultSessionWithPermissions(["integration-use-all"]),
+ });
+
+ await db.insert(integrations).values({
+ id: "1",
+ name: "Jellyfin",
+ kind: "jellyfin",
+ url: "http://jellyfin.local",
+ });
+
+ await expect(caller.searchMediaRequests({ query: "matrix", integrationIds: ["1"] })).rejects.toThrow(
+ "One or more media request search integrations were not found",
+ );
+ });
+
+ test("without query access to a scoped media search integration should throw forbidden", async () => {
+ const db = createDb();
+ const caller = integrationRouter.createCaller({
+ db,
+ deviceType: undefined,
+ session: defaultSessionWithPermissions(),
+ });
+
+ await db.insert(integrations).values({
+ id: "1",
+ name: "Jellyseerr",
+ kind: "jellyseerr",
+ url: "http://jellyseerr.local",
+ });
+
+ await expect(caller.searchMediaRequests({ query: "matrix", integrationIds: ["1"] })).rejects.toThrow(
+ "User does not have permission to query one or more media request search integrations",
+ );
+ });
+});
+
describe("byId should return an integration by id", () => {
test("with full access should return an integration by id", async () => {
const db = createDb();
@@ -208,7 +329,7 @@ describe("create should create a new integration", () => {
expect(dbSecret!.updatedAt).toEqual(fakeNow);
});
- test("with create integration access should create a new integration when creating search engine", async () => {
+ test("with create integration access should not create a search engine for media request search integrations", async () => {
const db = createDb();
const caller = integrationRouter.createCaller({
db,
@@ -243,12 +364,7 @@ describe("create should create a new integration", () => {
expect(dbSecret!.value).toMatch(/^[a-f0-9]+.[a-f0-9]+$/);
expect(dbSecret!.updatedAt).toEqual(fakeNow);
- expect(dbSearchEngine!.integrationId).toBe(dbIntegration!.id);
- expect(dbSearchEngine!.short).toBe("j");
- expect(dbSearchEngine!.name).toBe(input.name);
- expect(dbSearchEngine!.iconUrl).toBe(
- "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/jellyseerr.svg",
- );
+ expect(dbSearchEngine).toBeUndefined();
});
test("with create integration access should create a new integration with new linked app", async () => {
diff --git a/packages/api/src/router/test/user.spec.ts b/packages/api/src/router/test/user.spec.ts
index b3b262846..16cabfd52 100644
--- a/packages/api/src/router/test/user.spec.ts
+++ b/packages/api/src/router/test/user.spec.ts
@@ -81,13 +81,7 @@ describe("initUser should initialize the first user", () => {
await expect(actAsync()).rejects.toThrow("passwordsDoNotMatch");
});
- it.each([
- ["aB2%"], // too short
- ["abc123DEF"], // does not contain special characters
- ["abcDEFghi+"], // does not contain numbers
- ["ABC123+/-"], // does not contain lowercase
- ["abc123+/-"], // does not contain uppercase
- ])("should throw error that password requirements do not match for '%s' as password", async (password) => {
+ it.each([["aB2%"], ["short"]])("should reject passwords shorter than 8 characters for '%s'", async (password) => {
const db = createDb();
await createOnboardingStepAsync(db, "user");
const caller = userRouter.createCaller({
@@ -103,7 +97,55 @@ describe("initUser should initialize the first user", () => {
confirmPassword: password,
});
- await expect(actAsync()).rejects.toThrow("passwordRequirements");
+ await expect(actAsync()).rejects.toThrow();
+ });
+
+ it("should accept passwords without complexity requirements", async () => {
+ const db = createDb();
+ await createOnboardingStepAsync(db, "user");
+ const caller = userRouter.createCaller({
+ db,
+ deviceType: undefined,
+ session: null,
+ });
+
+ await caller.initUser({
+ username: "test",
+ password: "abc123DEF",
+ confirmPassword: "abc123DEF",
+ });
+
+ const user = await db.query.users.findFirst({
+ columns: {
+ id: true,
+ },
+ });
+
+ expect(user).toBeDefined();
+ });
+
+ it("should accept passwords with special characters like LoveHomarr<3", async () => {
+ const db = createDb();
+ await createOnboardingStepAsync(db, "user");
+ const caller = userRouter.createCaller({
+ db,
+ deviceType: undefined,
+ session: null,
+ });
+
+ await caller.initUser({
+ username: "test",
+ password: "LoveHomarr<3",
+ confirmPassword: "LoveHomarr<3",
+ });
+
+ const user = await db.query.users.findFirst({
+ columns: {
+ id: true,
+ },
+ });
+
+ expect(user).toBeDefined();
});
});
diff --git a/packages/api/src/router/user.ts b/packages/api/src/router/user.ts
index 9c4f16c80..466b095fd 100644
--- a/packages/api/src/router/user.ts
+++ b/packages/api/src/router/user.ts
@@ -32,6 +32,7 @@ import {
import { convertIntersectionToZodObject } from "../schema-merger";
import {
createTRPCRouter,
+ isDemoMode,
onboardingProcedure,
permissionRequiredProcedure,
protectedProcedure,
@@ -278,6 +279,8 @@ export const userRouter = createTRPCRouter({
defaultSearchEngineId: true,
openSearchInNewTab: true,
ddgBangs: true,
+ completedManageTour: true,
+ completedBoardTour: true,
}),
)
.meta({ openapi: { method: "GET", path: "/api/users/{userId}", tags: ["users"], protect: true } })
@@ -304,6 +307,8 @@ export const userRouter = createTRPCRouter({
defaultSearchEngineId: true,
openSearchInNewTab: true,
ddgBangs: true,
+ completedManageTour: true,
+ completedBoardTour: true,
},
where: eq(users.id, input.userId),
});
@@ -542,7 +547,7 @@ export const userRouter = createTRPCRouter({
.set({
pingIconsEnabled: input.pingIconsEnabled,
})
- .where(eq(users.id, ctx.session.user.id));
+ .where(eq(users.id, input.id));
}),
changeDdgBangs: protectedProcedure
.input(convertIntersectionToZodObject(userDdgBangsSchema.and(byIdSchema)))
@@ -603,8 +608,48 @@ export const userRouter = createTRPCRouter({
.set({
firstDayOfWeek: input.firstDayOfWeek,
})
- .where(eq(users.id, ctx.session.user.id));
+ .where(eq(users.id, input.id));
}),
+ completeTour: protectedProcedure
+ .input(z.object({ tour: z.enum(["manage", "board"]) }))
+ .mutation(async ({ input, ctx }) => {
+ const columnByTour = {
+ manage: { completedManageTour: true },
+ board: { completedBoardTour: true },
+ } as const;
+
+ await ctx.db.update(users).set(columnByTour[input.tour]).where(eq(users.id, ctx.session.user.id));
+ }),
+ resetTours: protectedProcedure.mutation(async ({ ctx }) => {
+ await ctx.db
+ .update(users)
+ .set({
+ completedManageTour: false,
+ completedBoardTour: false,
+ })
+ .where(eq(users.id, ctx.session.user.id));
+ }),
+ getTourStatus: protectedProcedure.query(async ({ ctx }) => {
+ if (isDemoMode) {
+ return {
+ completedManageTour: true,
+ completedBoardTour: true,
+ };
+ }
+
+ const user = await ctx.db.query.users.findFirst({
+ columns: {
+ completedManageTour: true,
+ completedBoardTour: true,
+ },
+ where: eq(users.id, ctx.session.user.id),
+ });
+
+ return {
+ completedManageTour: user?.completedManageTour ?? false,
+ completedBoardTour: user?.completedBoardTour ?? false,
+ };
+ }),
});
const createUserAsync = async (db: Database, input: Omit, "groupIds">) => {
diff --git a/packages/api/src/router/widgets/index.ts b/packages/api/src/router/widgets/index.ts
index e40c3f000..15f0963b4 100644
--- a/packages/api/src/router/widgets/index.ts
+++ b/packages/api/src/router/widgets/index.ts
@@ -31,4 +31,5 @@ export const widgetRouter = createTRPCRouter({
tracearr: lazy(() => import("./tracearr").then((mod) => mod.tracearrRouter)),
speedtestTracker: lazy(() => import("./speedtest-tracker").then((mod) => mod.speedtestTrackerRouter)),
umami: lazy(() => import("./umami").then((mod) => mod.umamiRouter)),
+ ups: lazy(() => import("./ups").then((mod) => mod.upsRouter)),
});
diff --git a/packages/api/src/router/widgets/ups.ts b/packages/api/src/router/widgets/ups.ts
new file mode 100644
index 000000000..f1c27c434
--- /dev/null
+++ b/packages/api/src/router/widgets/ups.ts
@@ -0,0 +1,51 @@
+import { observable } from "@trpc/server/observable";
+
+import { getIntegrationKindsByCategory } from "@homarr/definitions";
+import type { UpsSummary } from "@homarr/integrations/types";
+import { upsSummariesRequestHandler } from "@homarr/request-handler/ups";
+
+import { createManyIntegrationMiddleware } from "../../middlewares/integration";
+import { createTRPCRouter, publicProcedure } from "../../trpc";
+
+export const upsRouter = createTRPCRouter({
+ getSummaries: publicProcedure
+ .concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("ups")))
+ .query(async ({ ctx }) => {
+ const results = await Promise.all(
+ ctx.integrations.map(async (integration) => {
+ const innerHandler = upsSummariesRequestHandler.handler(integration, {});
+ const { data, timestamp } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
+
+ return {
+ integrationId: integration.id,
+ integrationName: integration.name,
+ integrationUrl: integration.url,
+ summaries: data,
+ updatedAt: timestamp,
+ };
+ }),
+ );
+
+ return results;
+ }),
+ subscribeSummaries: publicProcedure
+ .concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("ups")))
+ .subscription(({ ctx }) => {
+ return observable<{ integrationId: string; summaries: UpsSummary[]; timestamp: Date }>((emit) => {
+ const unsubscribes = ctx.integrations.map((integration) => {
+ const innerHandler = upsSummariesRequestHandler.handler(integration, {});
+ return innerHandler.subscribe((summaries) => {
+ emit.next({
+ integrationId: integration.id,
+ summaries,
+ timestamp: new Date(),
+ });
+ });
+ });
+
+ return () => {
+ unsubscribes.forEach((unsubscribe) => unsubscribe());
+ };
+ });
+ }),
+});
diff --git a/packages/api/src/trpc.ts b/packages/api/src/trpc.ts
index bedd60f3d..eb4da06c2 100644
--- a/packages/api/src/trpc.ts
+++ b/packages/api/src/trpc.ts
@@ -18,6 +18,7 @@ import { createLogger } from "@homarr/core/infrastructure/logs";
import { db } from "@homarr/db";
import type { GroupPermissionKey, OnboardingStep } from "@homarr/definitions";
+import { env } from "./env";
import { getOnboardingOrFallbackAsync } from "./router/onboard/onboard-queries";
const logger = createLogger({ module: "trpc" });
@@ -87,6 +88,20 @@ export const createCallerFactory = t.createCallerFactory;
*/
export const createTRPCRouter = t.router;
+export const isDemoMode = env.DEMO_MODE;
+
+const enforceDemoModeReadOnly = t.middleware(({ ctx, next, type }) => {
+ if (env.DEMO_MODE && type === "mutation") {
+ throw new TRPCError({
+ code: "FORBIDDEN",
+ message: "Mutations are disabled in demo mode",
+ });
+ }
+ return next({ ctx });
+});
+
+const baseProcedure = env.DEMO_MODE ? t.procedure.use(enforceDemoModeReadOnly) : t.procedure;
+
/**
* Public (unauthed) procedure
*
@@ -94,19 +109,14 @@ export const createTRPCRouter = t.router;
* tRPC API. It does not guarantee that a user querying is authorized, but you
* can still access user session data if they are logged in
*/
-export const publicProcedure = t.procedure;
+export const publicProcedure = baseProcedure;
-/**
- * Reusable middleware that enforces users are logged in before running the
- * procedure
- */
const enforceUserIsAuthed = t.middleware(({ ctx, next }) => {
if (!ctx.session?.user) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
return next({
ctx: {
- // infers the `session` as non-nullable
session: { ...ctx.session, user: ctx.session.user },
},
});
@@ -121,7 +131,7 @@ const enforceUserIsAuthed = t.middleware(({ ctx, next }) => {
*
* @see https://trpc.io/docs/procedures
*/
-export const protectedProcedure = t.procedure.use(enforceUserIsAuthed);
+export const protectedProcedure = baseProcedure.use(enforceUserIsAuthed);
/**
* Procedure that requires a specific permission
diff --git a/packages/common/src/url.ts b/packages/common/src/url.ts
index 7550def44..f13cc1efc 100644
--- a/packages/common/src/url.ts
+++ b/packages/common/src/url.ts
@@ -1,7 +1,11 @@
import type { ReadonlyHeaders } from "next/dist/server/web/spec-extension/adapters/headers";
export const removeTrailingSlash = (path: string) => {
- return path.at(-1) === "/" ? path.substring(0, path.length - 1) : path;
+ let end = path.length;
+ while (end > 0 && path.charAt(end - 1) === "/") {
+ end--;
+ }
+ return path.slice(0, end);
};
export const extractBaseUrlFromHeaders = (
diff --git a/packages/db/migrations/mysql/0039_add_user_onboarding_tour_status.sql b/packages/db/migrations/mysql/0039_add_user_onboarding_tour_status.sql
new file mode 100644
index 000000000..356540dc7
--- /dev/null
+++ b/packages/db/migrations/mysql/0039_add_user_onboarding_tour_status.sql
@@ -0,0 +1,2 @@
+ALTER TABLE `user` ADD `completed_manage_tour` boolean DEFAULT false NOT NULL;--> statement-breakpoint
+ALTER TABLE `user` ADD `completed_board_tour` boolean DEFAULT false NOT NULL;
\ No newline at end of file
diff --git a/packages/db/migrations/mysql/meta/0039_snapshot.json b/packages/db/migrations/mysql/meta/0039_snapshot.json
new file mode 100644
index 000000000..6630bd286
--- /dev/null
+++ b/packages/db/migrations/mysql/meta/0039_snapshot.json
@@ -0,0 +1,2120 @@
+{
+ "version": "5",
+ "dialect": "mysql",
+ "id": "e7c06822-4b94-49ea-b685-acb08ee6b63e",
+ "prevId": "ddg-bangs-mysql-0038",
+ "tables": {
+ "account": {
+ "name": "account",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "provider": {
+ "name": "provider",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "provider_account_id": {
+ "name": "provider_account_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "refresh_token": {
+ "name": "refresh_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "access_token": {
+ "name": "access_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "token_type": {
+ "name": "token_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "scope": {
+ "name": "scope",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "id_token": {
+ "name": "id_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "session_state": {
+ "name": "session_state",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "userId_idx": {
+ "name": "userId_idx",
+ "columns": ["user_id"],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "account_user_id_user_id_fk": {
+ "name": "account_user_id_user_id_fk",
+ "tableFrom": "account",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "account_provider_provider_account_id_pk": {
+ "name": "account_provider_provider_account_id_pk",
+ "columns": ["provider", "provider_account_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "apiKey": {
+ "name": "apiKey",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "api_key": {
+ "name": "api_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "apiKey_user_id_user_id_fk": {
+ "name": "apiKey_user_id_user_id_fk",
+ "tableFrom": "apiKey",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "apiKey_id": {
+ "name": "apiKey_id",
+ "columns": ["id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "app": {
+ "name": "app",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "icon_url": {
+ "name": "icon_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "href": {
+ "name": "href",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "ping_url": {
+ "name": "ping_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "app_id": {
+ "name": "app_id",
+ "columns": ["id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "boardGroupPermission": {
+ "name": "boardGroupPermission",
+ "columns": {
+ "board_id": {
+ "name": "board_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "group_id": {
+ "name": "group_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "permission": {
+ "name": "permission",
+ "type": "varchar(128)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "boardGroupPermission_board_id_board_id_fk": {
+ "name": "boardGroupPermission_board_id_board_id_fk",
+ "tableFrom": "boardGroupPermission",
+ "tableTo": "board",
+ "columnsFrom": ["board_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "boardGroupPermission_group_id_group_id_fk": {
+ "name": "boardGroupPermission_group_id_group_id_fk",
+ "tableFrom": "boardGroupPermission",
+ "tableTo": "group",
+ "columnsFrom": ["group_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "boardGroupPermission_board_id_group_id_permission_pk": {
+ "name": "boardGroupPermission_board_id_group_id_permission_pk",
+ "columns": ["board_id", "group_id", "permission"]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "boardUserPermission": {
+ "name": "boardUserPermission",
+ "columns": {
+ "board_id": {
+ "name": "board_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "permission": {
+ "name": "permission",
+ "type": "varchar(128)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "boardUserPermission_board_id_board_id_fk": {
+ "name": "boardUserPermission_board_id_board_id_fk",
+ "tableFrom": "boardUserPermission",
+ "tableTo": "board",
+ "columnsFrom": ["board_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "boardUserPermission_user_id_user_id_fk": {
+ "name": "boardUserPermission_user_id_user_id_fk",
+ "tableFrom": "boardUserPermission",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "boardUserPermission_board_id_user_id_permission_pk": {
+ "name": "boardUserPermission_board_id_user_id_permission_pk",
+ "columns": ["board_id", "user_id", "permission"]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "board": {
+ "name": "board",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(256)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "is_public": {
+ "name": "is_public",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ },
+ "creator_id": {
+ "name": "creator_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "page_title": {
+ "name": "page_title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "meta_title": {
+ "name": "meta_title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "logo_image_url": {
+ "name": "logo_image_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "favicon_image_url": {
+ "name": "favicon_image_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "background_image_url": {
+ "name": "background_image_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "background_image_attachment": {
+ "name": "background_image_attachment",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "('fixed')"
+ },
+ "background_image_repeat": {
+ "name": "background_image_repeat",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "('no-repeat')"
+ },
+ "background_image_size": {
+ "name": "background_image_size",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "('cover')"
+ },
+ "primary_color": {
+ "name": "primary_color",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "('#fa5252')"
+ },
+ "secondary_color": {
+ "name": "secondary_color",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "('#fd7e14')"
+ },
+ "opacity": {
+ "name": "opacity",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": 100
+ },
+ "custom_css": {
+ "name": "custom_css",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "icon_color": {
+ "name": "icon_color",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "item_radius": {
+ "name": "item_radius",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "('lg')"
+ },
+ "disable_status": {
+ "name": "disable_status",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "board_creator_id_user_id_fk": {
+ "name": "board_creator_id_user_id_fk",
+ "tableFrom": "board",
+ "tableTo": "user",
+ "columnsFrom": ["creator_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "board_id": {
+ "name": "board_id",
+ "columns": ["id"]
+ }
+ },
+ "uniqueConstraints": {
+ "board_name_unique": {
+ "name": "board_name_unique",
+ "columns": ["name"]
+ }
+ },
+ "checkConstraint": {}
+ },
+ "cron_job_configuration": {
+ "name": "cron_job_configuration",
+ "columns": {
+ "name": {
+ "name": "name",
+ "type": "varchar(256)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "cron_expression": {
+ "name": "cron_expression",
+ "type": "varchar(32)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "is_enabled": {
+ "name": "is_enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "cron_job_configuration_name": {
+ "name": "cron_job_configuration_name",
+ "columns": ["name"]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "groupMember": {
+ "name": "groupMember",
+ "columns": {
+ "group_id": {
+ "name": "group_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "groupMember_group_id_group_id_fk": {
+ "name": "groupMember_group_id_group_id_fk",
+ "tableFrom": "groupMember",
+ "tableTo": "group",
+ "columnsFrom": ["group_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "groupMember_user_id_user_id_fk": {
+ "name": "groupMember_user_id_user_id_fk",
+ "tableFrom": "groupMember",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "groupMember_group_id_user_id_pk": {
+ "name": "groupMember_group_id_user_id_pk",
+ "columns": ["group_id", "user_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "groupPermission": {
+ "name": "groupPermission",
+ "columns": {
+ "group_id": {
+ "name": "group_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "permission": {
+ "name": "permission",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "groupPermission_group_id_group_id_fk": {
+ "name": "groupPermission_group_id_group_id_fk",
+ "tableFrom": "groupPermission",
+ "tableTo": "group",
+ "columnsFrom": ["group_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "group": {
+ "name": "group",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "owner_id": {
+ "name": "owner_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "home_board_id": {
+ "name": "home_board_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "mobile_home_board_id": {
+ "name": "mobile_home_board_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "position": {
+ "name": "position",
+ "type": "smallint",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "group_owner_id_user_id_fk": {
+ "name": "group_owner_id_user_id_fk",
+ "tableFrom": "group",
+ "tableTo": "user",
+ "columnsFrom": ["owner_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "group_home_board_id_board_id_fk": {
+ "name": "group_home_board_id_board_id_fk",
+ "tableFrom": "group",
+ "tableTo": "board",
+ "columnsFrom": ["home_board_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "group_mobile_home_board_id_board_id_fk": {
+ "name": "group_mobile_home_board_id_board_id_fk",
+ "tableFrom": "group",
+ "tableTo": "board",
+ "columnsFrom": ["mobile_home_board_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "group_id": {
+ "name": "group_id",
+ "columns": ["id"]
+ }
+ },
+ "uniqueConstraints": {
+ "group_name_unique": {
+ "name": "group_name_unique",
+ "columns": ["name"]
+ }
+ },
+ "checkConstraint": {}
+ },
+ "iconRepository": {
+ "name": "iconRepository",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "slug": {
+ "name": "slug",
+ "type": "varchar(150)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "iconRepository_id": {
+ "name": "iconRepository_id",
+ "columns": ["id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "icon": {
+ "name": "icon",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(250)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "url": {
+ "name": "url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "checksum": {
+ "name": "checksum",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "icon_repository_id": {
+ "name": "icon_repository_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "icon_icon_repository_id_iconRepository_id_fk": {
+ "name": "icon_icon_repository_id_iconRepository_id_fk",
+ "tableFrom": "icon",
+ "tableTo": "iconRepository",
+ "columnsFrom": ["icon_repository_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "icon_id": {
+ "name": "icon_id",
+ "columns": ["id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "integrationGroupPermissions": {
+ "name": "integrationGroupPermissions",
+ "columns": {
+ "integration_id": {
+ "name": "integration_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "group_id": {
+ "name": "group_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "permission": {
+ "name": "permission",
+ "type": "varchar(128)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "integrationGroupPermissions_integration_id_integration_id_fk": {
+ "name": "integrationGroupPermissions_integration_id_integration_id_fk",
+ "tableFrom": "integrationGroupPermissions",
+ "tableTo": "integration",
+ "columnsFrom": ["integration_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "integrationGroupPermissions_group_id_group_id_fk": {
+ "name": "integrationGroupPermissions_group_id_group_id_fk",
+ "tableFrom": "integrationGroupPermissions",
+ "tableTo": "group",
+ "columnsFrom": ["group_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "integration_group_permission__pk": {
+ "name": "integration_group_permission__pk",
+ "columns": ["integration_id", "group_id", "permission"]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "integration_item": {
+ "name": "integration_item",
+ "columns": {
+ "item_id": {
+ "name": "item_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "integration_id": {
+ "name": "integration_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "integration_item_item_id_item_id_fk": {
+ "name": "integration_item_item_id_item_id_fk",
+ "tableFrom": "integration_item",
+ "tableTo": "item",
+ "columnsFrom": ["item_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "integration_item_integration_id_integration_id_fk": {
+ "name": "integration_item_integration_id_integration_id_fk",
+ "tableFrom": "integration_item",
+ "tableTo": "integration",
+ "columnsFrom": ["integration_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "integration_item_item_id_integration_id_pk": {
+ "name": "integration_item_item_id_integration_id_pk",
+ "columns": ["item_id", "integration_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "integrationSecret": {
+ "name": "integrationSecret",
+ "columns": {
+ "kind": {
+ "name": "kind",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "value": {
+ "name": "value",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "integration_id": {
+ "name": "integration_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "integration_secret__kind_idx": {
+ "name": "integration_secret__kind_idx",
+ "columns": ["kind"],
+ "isUnique": false
+ },
+ "integration_secret__updated_at_idx": {
+ "name": "integration_secret__updated_at_idx",
+ "columns": ["updated_at"],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "integrationSecret_integration_id_integration_id_fk": {
+ "name": "integrationSecret_integration_id_integration_id_fk",
+ "tableFrom": "integrationSecret",
+ "tableTo": "integration",
+ "columnsFrom": ["integration_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "integrationSecret_integration_id_kind_pk": {
+ "name": "integrationSecret_integration_id_kind_pk",
+ "columns": ["integration_id", "kind"]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "integrationUserPermission": {
+ "name": "integrationUserPermission",
+ "columns": {
+ "integration_id": {
+ "name": "integration_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "permission": {
+ "name": "permission",
+ "type": "varchar(128)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "integrationUserPermission_integration_id_integration_id_fk": {
+ "name": "integrationUserPermission_integration_id_integration_id_fk",
+ "tableFrom": "integrationUserPermission",
+ "tableTo": "integration",
+ "columnsFrom": ["integration_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "integrationUserPermission_user_id_user_id_fk": {
+ "name": "integrationUserPermission_user_id_user_id_fk",
+ "tableFrom": "integrationUserPermission",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "integrationUserPermission_integration_id_user_id_permission_pk": {
+ "name": "integrationUserPermission_integration_id_user_id_permission_pk",
+ "columns": ["integration_id", "user_id", "permission"]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "integration": {
+ "name": "integration",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "url": {
+ "name": "url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "kind": {
+ "name": "kind",
+ "type": "varchar(128)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "app_id": {
+ "name": "app_id",
+ "type": "varchar(128)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "integration__kind_idx": {
+ "name": "integration__kind_idx",
+ "columns": ["kind"],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "integration_app_id_app_id_fk": {
+ "name": "integration_app_id_app_id_fk",
+ "tableFrom": "integration",
+ "tableTo": "app",
+ "columnsFrom": ["app_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "integration_id": {
+ "name": "integration_id",
+ "columns": ["id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "invite": {
+ "name": "invite",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "token": {
+ "name": "token",
+ "type": "varchar(512)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "expiration_date": {
+ "name": "expiration_date",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "creator_id": {
+ "name": "creator_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "invite_creator_id_user_id_fk": {
+ "name": "invite_creator_id_user_id_fk",
+ "tableFrom": "invite",
+ "tableTo": "user",
+ "columnsFrom": ["creator_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "invite_id": {
+ "name": "invite_id",
+ "columns": ["id"]
+ }
+ },
+ "uniqueConstraints": {
+ "invite_token_unique": {
+ "name": "invite_token_unique",
+ "columns": ["token"]
+ }
+ },
+ "checkConstraint": {}
+ },
+ "item_layout": {
+ "name": "item_layout",
+ "columns": {
+ "item_id": {
+ "name": "item_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "section_id": {
+ "name": "section_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "layout_id": {
+ "name": "layout_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "x_offset": {
+ "name": "x_offset",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "y_offset": {
+ "name": "y_offset",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "width": {
+ "name": "width",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "height": {
+ "name": "height",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "item_layout_item_id_item_id_fk": {
+ "name": "item_layout_item_id_item_id_fk",
+ "tableFrom": "item_layout",
+ "tableTo": "item",
+ "columnsFrom": ["item_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "item_layout_section_id_section_id_fk": {
+ "name": "item_layout_section_id_section_id_fk",
+ "tableFrom": "item_layout",
+ "tableTo": "section",
+ "columnsFrom": ["section_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "item_layout_layout_id_layout_id_fk": {
+ "name": "item_layout_layout_id_layout_id_fk",
+ "tableFrom": "item_layout",
+ "tableTo": "layout",
+ "columnsFrom": ["layout_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "item_layout_item_id_section_id_layout_id_pk": {
+ "name": "item_layout_item_id_section_id_layout_id_pk",
+ "columns": ["item_id", "section_id", "layout_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "item": {
+ "name": "item",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "board_id": {
+ "name": "board_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "kind": {
+ "name": "kind",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "options": {
+ "name": "options",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "('{\"json\": {}}')"
+ },
+ "advanced_options": {
+ "name": "advanced_options",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "('{\"json\": {}}')"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "item_board_id_board_id_fk": {
+ "name": "item_board_id_board_id_fk",
+ "tableFrom": "item",
+ "tableTo": "board",
+ "columnsFrom": ["board_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "item_id": {
+ "name": "item_id",
+ "columns": ["id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "layout": {
+ "name": "layout",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(32)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "board_id": {
+ "name": "board_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "column_count": {
+ "name": "column_count",
+ "type": "tinyint",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "breakpoint": {
+ "name": "breakpoint",
+ "type": "smallint",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": 0
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "layout_board_id_board_id_fk": {
+ "name": "layout_board_id_board_id_fk",
+ "tableFrom": "layout",
+ "tableTo": "board",
+ "columnsFrom": ["board_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "layout_id": {
+ "name": "layout_id",
+ "columns": ["id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "media": {
+ "name": "media",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(512)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "content": {
+ "name": "content",
+ "type": "LONGBLOB",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "content_type": {
+ "name": "content_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "size": {
+ "name": "size",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(now())"
+ },
+ "creator_id": {
+ "name": "creator_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "media_creator_id_user_id_fk": {
+ "name": "media_creator_id_user_id_fk",
+ "tableFrom": "media",
+ "tableTo": "user",
+ "columnsFrom": ["creator_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "media_id": {
+ "name": "media_id",
+ "columns": ["id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "onboarding": {
+ "name": "onboarding",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "step": {
+ "name": "step",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "previous_step": {
+ "name": "previous_step",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "onboarding_id": {
+ "name": "onboarding_id",
+ "columns": ["id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "search_engine": {
+ "name": "search_engine",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "icon_url": {
+ "name": "icon_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "short": {
+ "name": "short",
+ "type": "varchar(8)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "url_template": {
+ "name": "url_template",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "type": {
+ "name": "type",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'generic'"
+ },
+ "integration_id": {
+ "name": "integration_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "search_engine_integration_id_integration_id_fk": {
+ "name": "search_engine_integration_id_integration_id_fk",
+ "tableFrom": "search_engine",
+ "tableTo": "integration",
+ "columnsFrom": ["integration_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "search_engine_id": {
+ "name": "search_engine_id",
+ "columns": ["id"]
+ }
+ },
+ "uniqueConstraints": {
+ "search_engine_short_unique": {
+ "name": "search_engine_short_unique",
+ "columns": ["short"]
+ }
+ },
+ "checkConstraint": {}
+ },
+ "section_collapse_state": {
+ "name": "section_collapse_state",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "section_id": {
+ "name": "section_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "collapsed": {
+ "name": "collapsed",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "section_collapse_state_user_id_user_id_fk": {
+ "name": "section_collapse_state_user_id_user_id_fk",
+ "tableFrom": "section_collapse_state",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "section_collapse_state_section_id_section_id_fk": {
+ "name": "section_collapse_state_section_id_section_id_fk",
+ "tableFrom": "section_collapse_state",
+ "tableTo": "section",
+ "columnsFrom": ["section_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "section_collapse_state_user_id_section_id_pk": {
+ "name": "section_collapse_state_user_id_section_id_pk",
+ "columns": ["user_id", "section_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "section_layout": {
+ "name": "section_layout",
+ "columns": {
+ "section_id": {
+ "name": "section_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "layout_id": {
+ "name": "layout_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "parent_section_id": {
+ "name": "parent_section_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "x_offset": {
+ "name": "x_offset",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "y_offset": {
+ "name": "y_offset",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "width": {
+ "name": "width",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "height": {
+ "name": "height",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "section_layout_section_id_section_id_fk": {
+ "name": "section_layout_section_id_section_id_fk",
+ "tableFrom": "section_layout",
+ "tableTo": "section",
+ "columnsFrom": ["section_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "section_layout_layout_id_layout_id_fk": {
+ "name": "section_layout_layout_id_layout_id_fk",
+ "tableFrom": "section_layout",
+ "tableTo": "layout",
+ "columnsFrom": ["layout_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "section_layout_parent_section_id_section_id_fk": {
+ "name": "section_layout_parent_section_id_section_id_fk",
+ "tableFrom": "section_layout",
+ "tableTo": "section",
+ "columnsFrom": ["parent_section_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "section_layout_section_id_layout_id_pk": {
+ "name": "section_layout_section_id_layout_id_pk",
+ "columns": ["section_id", "layout_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "section": {
+ "name": "section",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "board_id": {
+ "name": "board_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "kind": {
+ "name": "kind",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "x_offset": {
+ "name": "x_offset",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "y_offset": {
+ "name": "y_offset",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "options": {
+ "name": "options",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "('{\"json\": {}}')"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "section_board_id_board_id_fk": {
+ "name": "section_board_id_board_id_fk",
+ "tableFrom": "section",
+ "tableTo": "board",
+ "columnsFrom": ["board_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "section_id": {
+ "name": "section_id",
+ "columns": ["id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "serverSetting": {
+ "name": "serverSetting",
+ "columns": {
+ "setting_key": {
+ "name": "setting_key",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "value": {
+ "name": "value",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "('{\"json\": {}}')"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "serverSetting_setting_key": {
+ "name": "serverSetting_setting_key",
+ "columns": ["setting_key"]
+ }
+ },
+ "uniqueConstraints": {
+ "serverSetting_settingKey_unique": {
+ "name": "serverSetting_settingKey_unique",
+ "columns": ["setting_key"]
+ }
+ },
+ "checkConstraint": {}
+ },
+ "session": {
+ "name": "session",
+ "columns": {
+ "session_token": {
+ "name": "session_token",
+ "type": "varchar(512)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "expires": {
+ "name": "expires",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "user_id_idx": {
+ "name": "user_id_idx",
+ "columns": ["user_id"],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "session_user_id_user_id_fk": {
+ "name": "session_user_id_user_id_fk",
+ "tableFrom": "session",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "session_session_token": {
+ "name": "session_session_token",
+ "columns": ["session_token"]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "user": {
+ "name": "user",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "email_verified": {
+ "name": "email_verified",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "image": {
+ "name": "image",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "password": {
+ "name": "password",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "provider": {
+ "name": "provider",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'credentials'"
+ },
+ "home_board_id": {
+ "name": "home_board_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "mobile_home_board_id": {
+ "name": "mobile_home_board_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "default_search_engine_id": {
+ "name": "default_search_engine_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "open_search_in_new_tab": {
+ "name": "open_search_in_new_tab",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ },
+ "ddg_bangs": {
+ "name": "ddg_bangs",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": true
+ },
+ "color_scheme": {
+ "name": "color_scheme",
+ "type": "varchar(5)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'dark'"
+ },
+ "first_day_of_week": {
+ "name": "first_day_of_week",
+ "type": "tinyint",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": 1
+ },
+ "ping_icons_enabled": {
+ "name": "ping_icons_enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ },
+ "completed_manage_tour": {
+ "name": "completed_manage_tour",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ },
+ "completed_board_tour": {
+ "name": "completed_board_tour",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "user_home_board_id_board_id_fk": {
+ "name": "user_home_board_id_board_id_fk",
+ "tableFrom": "user",
+ "tableTo": "board",
+ "columnsFrom": ["home_board_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "user_mobile_home_board_id_board_id_fk": {
+ "name": "user_mobile_home_board_id_board_id_fk",
+ "tableFrom": "user",
+ "tableTo": "board",
+ "columnsFrom": ["mobile_home_board_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "user_default_search_engine_id_search_engine_id_fk": {
+ "name": "user_default_search_engine_id_search_engine_id_fk",
+ "tableFrom": "user",
+ "tableTo": "search_engine",
+ "columnsFrom": ["default_search_engine_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "user_id": {
+ "name": "user_id",
+ "columns": ["id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "verificationToken": {
+ "name": "verificationToken",
+ "columns": {
+ "identifier": {
+ "name": "identifier",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "token": {
+ "name": "token",
+ "type": "varchar(512)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "expires": {
+ "name": "expires",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "verificationToken_identifier_token_pk": {
+ "name": "verificationToken_identifier_token_pk",
+ "columns": ["identifier", "token"]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "trusted_certificate_hostname": {
+ "name": "trusted_certificate_hostname",
+ "columns": {
+ "hostname": {
+ "name": "hostname",
+ "type": "varchar(256)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "thumbprint": {
+ "name": "thumbprint",
+ "type": "varchar(128)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "certificate": {
+ "name": "certificate",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "trusted_certificate_hostname_hostname_thumbprint_pk": {
+ "name": "trusted_certificate_hostname_hostname_thumbprint_pk",
+ "columns": ["hostname", "thumbprint"]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ }
+ },
+ "views": {},
+ "_meta": {
+ "schemas": {},
+ "tables": {},
+ "columns": {}
+ },
+ "internal": {
+ "tables": {},
+ "indexes": {}
+ }
+}
diff --git a/packages/db/migrations/mysql/meta/_journal.json b/packages/db/migrations/mysql/meta/_journal.json
index 421c81699..f420759ec 100644
--- a/packages/db/migrations/mysql/meta/_journal.json
+++ b/packages/db/migrations/mysql/meta/_journal.json
@@ -274,6 +274,13 @@
"when": 1775400000000,
"tag": "0038_add_user_ddg_bangs",
"breakpoints": true
+ },
+ {
+ "idx": 39,
+ "version": "5",
+ "when": 1779199104477,
+ "tag": "0039_add_user_onboarding_tour_status",
+ "breakpoints": true
}
]
}
diff --git a/packages/db/migrations/postgresql/0005_add_user_onboarding_tour_status.sql b/packages/db/migrations/postgresql/0005_add_user_onboarding_tour_status.sql
new file mode 100644
index 000000000..dda51181e
--- /dev/null
+++ b/packages/db/migrations/postgresql/0005_add_user_onboarding_tour_status.sql
@@ -0,0 +1,2 @@
+ALTER TABLE "user" ADD COLUMN "completed_manage_tour" boolean DEFAULT false NOT NULL;--> statement-breakpoint
+ALTER TABLE "user" ADD COLUMN "completed_board_tour" boolean DEFAULT false NOT NULL;
\ No newline at end of file
diff --git a/packages/db/migrations/postgresql/meta/0005_snapshot.json b/packages/db/migrations/postgresql/meta/0005_snapshot.json
new file mode 100644
index 000000000..759659315
--- /dev/null
+++ b/packages/db/migrations/postgresql/meta/0005_snapshot.json
@@ -0,0 +1,2016 @@
+{
+ "id": "2869b3b7-af54-4fbc-a953-766387ec2e3d",
+ "prevId": "ddg-bangs-pg-0004",
+ "version": "7",
+ "dialect": "postgresql",
+ "tables": {
+ "public.account": {
+ "name": "account",
+ "schema": "",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider": {
+ "name": "provider",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider_account_id": {
+ "name": "provider_account_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "refresh_token": {
+ "name": "refresh_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "access_token": {
+ "name": "access_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "token_type": {
+ "name": "token_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "scope": {
+ "name": "scope",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "id_token": {
+ "name": "id_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "session_state": {
+ "name": "session_state",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "userId_idx": {
+ "name": "userId_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "account_user_id_user_id_fk": {
+ "name": "account_user_id_user_id_fk",
+ "tableFrom": "account",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "account_provider_provider_account_id_pk": {
+ "name": "account_provider_provider_account_id_pk",
+ "columns": ["provider", "provider_account_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.apiKey": {
+ "name": "apiKey",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(64)",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "api_key": {
+ "name": "api_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "apiKey_user_id_user_id_fk": {
+ "name": "apiKey_user_id_user_id_fk",
+ "tableFrom": "apiKey",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.app": {
+ "name": "app",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(64)",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "icon_url": {
+ "name": "icon_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "href": {
+ "name": "href",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "ping_url": {
+ "name": "ping_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.boardGroupPermission": {
+ "name": "boardGroupPermission",
+ "schema": "",
+ "columns": {
+ "board_id": {
+ "name": "board_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "group_id": {
+ "name": "group_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "permission": {
+ "name": "permission",
+ "type": "varchar(128)",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "boardGroupPermission_board_id_board_id_fk": {
+ "name": "boardGroupPermission_board_id_board_id_fk",
+ "tableFrom": "boardGroupPermission",
+ "tableTo": "board",
+ "columnsFrom": ["board_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "boardGroupPermission_group_id_group_id_fk": {
+ "name": "boardGroupPermission_group_id_group_id_fk",
+ "tableFrom": "boardGroupPermission",
+ "tableTo": "group",
+ "columnsFrom": ["group_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "boardGroupPermission_board_id_group_id_permission_pk": {
+ "name": "boardGroupPermission_board_id_group_id_permission_pk",
+ "columns": ["board_id", "group_id", "permission"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.boardUserPermission": {
+ "name": "boardUserPermission",
+ "schema": "",
+ "columns": {
+ "board_id": {
+ "name": "board_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "permission": {
+ "name": "permission",
+ "type": "varchar(128)",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "boardUserPermission_board_id_board_id_fk": {
+ "name": "boardUserPermission_board_id_board_id_fk",
+ "tableFrom": "boardUserPermission",
+ "tableTo": "board",
+ "columnsFrom": ["board_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "boardUserPermission_user_id_user_id_fk": {
+ "name": "boardUserPermission_user_id_user_id_fk",
+ "tableFrom": "boardUserPermission",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "boardUserPermission_board_id_user_id_permission_pk": {
+ "name": "boardUserPermission_board_id_user_id_permission_pk",
+ "columns": ["board_id", "user_id", "permission"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.board": {
+ "name": "board",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(64)",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(256)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "is_public": {
+ "name": "is_public",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "creator_id": {
+ "name": "creator_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "page_title": {
+ "name": "page_title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "meta_title": {
+ "name": "meta_title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "logo_image_url": {
+ "name": "logo_image_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "favicon_image_url": {
+ "name": "favicon_image_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "background_image_url": {
+ "name": "background_image_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "background_image_attachment": {
+ "name": "background_image_attachment",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'fixed'"
+ },
+ "background_image_repeat": {
+ "name": "background_image_repeat",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'no-repeat'"
+ },
+ "background_image_size": {
+ "name": "background_image_size",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'cover'"
+ },
+ "primary_color": {
+ "name": "primary_color",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'#fa5252'"
+ },
+ "secondary_color": {
+ "name": "secondary_color",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'#fd7e14'"
+ },
+ "opacity": {
+ "name": "opacity",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 100
+ },
+ "custom_css": {
+ "name": "custom_css",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "icon_color": {
+ "name": "icon_color",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "item_radius": {
+ "name": "item_radius",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'lg'"
+ },
+ "disable_status": {
+ "name": "disable_status",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "board_creator_id_user_id_fk": {
+ "name": "board_creator_id_user_id_fk",
+ "tableFrom": "board",
+ "tableTo": "user",
+ "columnsFrom": ["creator_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "board_name_unique": {
+ "name": "board_name_unique",
+ "nullsNotDistinct": false,
+ "columns": ["name"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.cron_job_configuration": {
+ "name": "cron_job_configuration",
+ "schema": "",
+ "columns": {
+ "name": {
+ "name": "name",
+ "type": "varchar(256)",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "cron_expression": {
+ "name": "cron_expression",
+ "type": "varchar(32)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "is_enabled": {
+ "name": "is_enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.groupMember": {
+ "name": "groupMember",
+ "schema": "",
+ "columns": {
+ "group_id": {
+ "name": "group_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "groupMember_group_id_group_id_fk": {
+ "name": "groupMember_group_id_group_id_fk",
+ "tableFrom": "groupMember",
+ "tableTo": "group",
+ "columnsFrom": ["group_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "groupMember_user_id_user_id_fk": {
+ "name": "groupMember_user_id_user_id_fk",
+ "tableFrom": "groupMember",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "groupMember_group_id_user_id_pk": {
+ "name": "groupMember_group_id_user_id_pk",
+ "columns": ["group_id", "user_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.groupPermission": {
+ "name": "groupPermission",
+ "schema": "",
+ "columns": {
+ "group_id": {
+ "name": "group_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "permission": {
+ "name": "permission",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "groupPermission_group_id_group_id_fk": {
+ "name": "groupPermission_group_id_group_id_fk",
+ "tableFrom": "groupPermission",
+ "tableTo": "group",
+ "columnsFrom": ["group_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.group": {
+ "name": "group",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(64)",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "owner_id": {
+ "name": "owner_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "home_board_id": {
+ "name": "home_board_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "mobile_home_board_id": {
+ "name": "mobile_home_board_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "position": {
+ "name": "position",
+ "type": "smallint",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "group_owner_id_user_id_fk": {
+ "name": "group_owner_id_user_id_fk",
+ "tableFrom": "group",
+ "tableTo": "user",
+ "columnsFrom": ["owner_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "group_home_board_id_board_id_fk": {
+ "name": "group_home_board_id_board_id_fk",
+ "tableFrom": "group",
+ "tableTo": "board",
+ "columnsFrom": ["home_board_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "group_mobile_home_board_id_board_id_fk": {
+ "name": "group_mobile_home_board_id_board_id_fk",
+ "tableFrom": "group",
+ "tableTo": "board",
+ "columnsFrom": ["mobile_home_board_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "group_name_unique": {
+ "name": "group_name_unique",
+ "nullsNotDistinct": false,
+ "columns": ["name"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.iconRepository": {
+ "name": "iconRepository",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(64)",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "slug": {
+ "name": "slug",
+ "type": "varchar(150)",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.icon": {
+ "name": "icon",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(64)",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(250)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "url": {
+ "name": "url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "checksum": {
+ "name": "checksum",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "icon_repository_id": {
+ "name": "icon_repository_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "icon_icon_repository_id_iconRepository_id_fk": {
+ "name": "icon_icon_repository_id_iconRepository_id_fk",
+ "tableFrom": "icon",
+ "tableTo": "iconRepository",
+ "columnsFrom": ["icon_repository_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.integrationGroupPermissions": {
+ "name": "integrationGroupPermissions",
+ "schema": "",
+ "columns": {
+ "integration_id": {
+ "name": "integration_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "group_id": {
+ "name": "group_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "permission": {
+ "name": "permission",
+ "type": "varchar(128)",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "integrationGroupPermissions_integration_id_integration_id_fk": {
+ "name": "integrationGroupPermissions_integration_id_integration_id_fk",
+ "tableFrom": "integrationGroupPermissions",
+ "tableTo": "integration",
+ "columnsFrom": ["integration_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "integrationGroupPermissions_group_id_group_id_fk": {
+ "name": "integrationGroupPermissions_group_id_group_id_fk",
+ "tableFrom": "integrationGroupPermissions",
+ "tableTo": "group",
+ "columnsFrom": ["group_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "integration_group_permission__pk": {
+ "name": "integration_group_permission__pk",
+ "columns": ["integration_id", "group_id", "permission"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.integration_item": {
+ "name": "integration_item",
+ "schema": "",
+ "columns": {
+ "item_id": {
+ "name": "item_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "integration_id": {
+ "name": "integration_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "integration_item_item_id_item_id_fk": {
+ "name": "integration_item_item_id_item_id_fk",
+ "tableFrom": "integration_item",
+ "tableTo": "item",
+ "columnsFrom": ["item_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "integration_item_integration_id_integration_id_fk": {
+ "name": "integration_item_integration_id_integration_id_fk",
+ "tableFrom": "integration_item",
+ "tableTo": "integration",
+ "columnsFrom": ["integration_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "integration_item_item_id_integration_id_pk": {
+ "name": "integration_item_item_id_integration_id_pk",
+ "columns": ["item_id", "integration_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.integrationSecret": {
+ "name": "integrationSecret",
+ "schema": "",
+ "columns": {
+ "kind": {
+ "name": "kind",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "value": {
+ "name": "value",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "integration_id": {
+ "name": "integration_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "integration_secret__kind_idx": {
+ "name": "integration_secret__kind_idx",
+ "columns": [
+ {
+ "expression": "kind",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "integration_secret__updated_at_idx": {
+ "name": "integration_secret__updated_at_idx",
+ "columns": [
+ {
+ "expression": "updated_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "integrationSecret_integration_id_integration_id_fk": {
+ "name": "integrationSecret_integration_id_integration_id_fk",
+ "tableFrom": "integrationSecret",
+ "tableTo": "integration",
+ "columnsFrom": ["integration_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "integrationSecret_integration_id_kind_pk": {
+ "name": "integrationSecret_integration_id_kind_pk",
+ "columns": ["integration_id", "kind"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.integrationUserPermission": {
+ "name": "integrationUserPermission",
+ "schema": "",
+ "columns": {
+ "integration_id": {
+ "name": "integration_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "permission": {
+ "name": "permission",
+ "type": "varchar(128)",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "integrationUserPermission_integration_id_integration_id_fk": {
+ "name": "integrationUserPermission_integration_id_integration_id_fk",
+ "tableFrom": "integrationUserPermission",
+ "tableTo": "integration",
+ "columnsFrom": ["integration_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "integrationUserPermission_user_id_user_id_fk": {
+ "name": "integrationUserPermission_user_id_user_id_fk",
+ "tableFrom": "integrationUserPermission",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "integrationUserPermission_integration_id_user_id_permission_pk": {
+ "name": "integrationUserPermission_integration_id_user_id_permission_pk",
+ "columns": ["integration_id", "user_id", "permission"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.integration": {
+ "name": "integration",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(64)",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "url": {
+ "name": "url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "kind": {
+ "name": "kind",
+ "type": "varchar(128)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "app_id": {
+ "name": "app_id",
+ "type": "varchar(128)",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "integration__kind_idx": {
+ "name": "integration__kind_idx",
+ "columns": [
+ {
+ "expression": "kind",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "integration_app_id_app_id_fk": {
+ "name": "integration_app_id_app_id_fk",
+ "tableFrom": "integration",
+ "tableTo": "app",
+ "columnsFrom": ["app_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.invite": {
+ "name": "invite",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(64)",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "token": {
+ "name": "token",
+ "type": "varchar(512)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expiration_date": {
+ "name": "expiration_date",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "creator_id": {
+ "name": "creator_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "invite_creator_id_user_id_fk": {
+ "name": "invite_creator_id_user_id_fk",
+ "tableFrom": "invite",
+ "tableTo": "user",
+ "columnsFrom": ["creator_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "invite_token_unique": {
+ "name": "invite_token_unique",
+ "nullsNotDistinct": false,
+ "columns": ["token"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.item_layout": {
+ "name": "item_layout",
+ "schema": "",
+ "columns": {
+ "item_id": {
+ "name": "item_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "section_id": {
+ "name": "section_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "layout_id": {
+ "name": "layout_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "x_offset": {
+ "name": "x_offset",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "y_offset": {
+ "name": "y_offset",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "width": {
+ "name": "width",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "height": {
+ "name": "height",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "item_layout_item_id_item_id_fk": {
+ "name": "item_layout_item_id_item_id_fk",
+ "tableFrom": "item_layout",
+ "tableTo": "item",
+ "columnsFrom": ["item_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "item_layout_section_id_section_id_fk": {
+ "name": "item_layout_section_id_section_id_fk",
+ "tableFrom": "item_layout",
+ "tableTo": "section",
+ "columnsFrom": ["section_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "item_layout_layout_id_layout_id_fk": {
+ "name": "item_layout_layout_id_layout_id_fk",
+ "tableFrom": "item_layout",
+ "tableTo": "layout",
+ "columnsFrom": ["layout_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "item_layout_item_id_section_id_layout_id_pk": {
+ "name": "item_layout_item_id_section_id_layout_id_pk",
+ "columns": ["item_id", "section_id", "layout_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.item": {
+ "name": "item",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(64)",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "board_id": {
+ "name": "board_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "kind": {
+ "name": "kind",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "options": {
+ "name": "options",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{\"json\": {}}'"
+ },
+ "advanced_options": {
+ "name": "advanced_options",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{\"json\": {}}'"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "item_board_id_board_id_fk": {
+ "name": "item_board_id_board_id_fk",
+ "tableFrom": "item",
+ "tableTo": "board",
+ "columnsFrom": ["board_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.layout": {
+ "name": "layout",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(64)",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(32)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "board_id": {
+ "name": "board_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "column_count": {
+ "name": "column_count",
+ "type": "smallint",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "breakpoint": {
+ "name": "breakpoint",
+ "type": "smallint",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "layout_board_id_board_id_fk": {
+ "name": "layout_board_id_board_id_fk",
+ "tableFrom": "layout",
+ "tableTo": "board",
+ "columnsFrom": ["board_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.media": {
+ "name": "media",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(64)",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(512)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "content": {
+ "name": "content",
+ "type": "bytea",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "content_type": {
+ "name": "content_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "size": {
+ "name": "size",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "creator_id": {
+ "name": "creator_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "media_creator_id_user_id_fk": {
+ "name": "media_creator_id_user_id_fk",
+ "tableFrom": "media",
+ "tableTo": "user",
+ "columnsFrom": ["creator_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.onboarding": {
+ "name": "onboarding",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(64)",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "step": {
+ "name": "step",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "previous_step": {
+ "name": "previous_step",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.search_engine": {
+ "name": "search_engine",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(64)",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "icon_url": {
+ "name": "icon_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "short": {
+ "name": "short",
+ "type": "varchar(8)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "url_template": {
+ "name": "url_template",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "type": {
+ "name": "type",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'generic'"
+ },
+ "integration_id": {
+ "name": "integration_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "search_engine_integration_id_integration_id_fk": {
+ "name": "search_engine_integration_id_integration_id_fk",
+ "tableFrom": "search_engine",
+ "tableTo": "integration",
+ "columnsFrom": ["integration_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "search_engine_short_unique": {
+ "name": "search_engine_short_unique",
+ "nullsNotDistinct": false,
+ "columns": ["short"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.section_collapse_state": {
+ "name": "section_collapse_state",
+ "schema": "",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "section_id": {
+ "name": "section_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "collapsed": {
+ "name": "collapsed",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "section_collapse_state_user_id_user_id_fk": {
+ "name": "section_collapse_state_user_id_user_id_fk",
+ "tableFrom": "section_collapse_state",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "section_collapse_state_section_id_section_id_fk": {
+ "name": "section_collapse_state_section_id_section_id_fk",
+ "tableFrom": "section_collapse_state",
+ "tableTo": "section",
+ "columnsFrom": ["section_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "section_collapse_state_user_id_section_id_pk": {
+ "name": "section_collapse_state_user_id_section_id_pk",
+ "columns": ["user_id", "section_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.section_layout": {
+ "name": "section_layout",
+ "schema": "",
+ "columns": {
+ "section_id": {
+ "name": "section_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "layout_id": {
+ "name": "layout_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "parent_section_id": {
+ "name": "parent_section_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "x_offset": {
+ "name": "x_offset",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "y_offset": {
+ "name": "y_offset",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "width": {
+ "name": "width",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "height": {
+ "name": "height",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "section_layout_section_id_section_id_fk": {
+ "name": "section_layout_section_id_section_id_fk",
+ "tableFrom": "section_layout",
+ "tableTo": "section",
+ "columnsFrom": ["section_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "section_layout_layout_id_layout_id_fk": {
+ "name": "section_layout_layout_id_layout_id_fk",
+ "tableFrom": "section_layout",
+ "tableTo": "layout",
+ "columnsFrom": ["layout_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "section_layout_parent_section_id_section_id_fk": {
+ "name": "section_layout_parent_section_id_section_id_fk",
+ "tableFrom": "section_layout",
+ "tableTo": "section",
+ "columnsFrom": ["parent_section_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "section_layout_section_id_layout_id_pk": {
+ "name": "section_layout_section_id_layout_id_pk",
+ "columns": ["section_id", "layout_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.section": {
+ "name": "section",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(64)",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "board_id": {
+ "name": "board_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "kind": {
+ "name": "kind",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "x_offset": {
+ "name": "x_offset",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "y_offset": {
+ "name": "y_offset",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "options": {
+ "name": "options",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'{\"json\": {}}'"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "section_board_id_board_id_fk": {
+ "name": "section_board_id_board_id_fk",
+ "tableFrom": "section",
+ "tableTo": "board",
+ "columnsFrom": ["board_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.serverSetting": {
+ "name": "serverSetting",
+ "schema": "",
+ "columns": {
+ "setting_key": {
+ "name": "setting_key",
+ "type": "varchar(64)",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "value": {
+ "name": "value",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{\"json\": {}}'"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "serverSetting_settingKey_unique": {
+ "name": "serverSetting_settingKey_unique",
+ "nullsNotDistinct": false,
+ "columns": ["setting_key"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.session": {
+ "name": "session",
+ "schema": "",
+ "columns": {
+ "session_token": {
+ "name": "session_token",
+ "type": "varchar(512)",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires": {
+ "name": "expires",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "user_id_idx": {
+ "name": "user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "session_user_id_user_id_fk": {
+ "name": "session_user_id_user_id_fk",
+ "tableFrom": "session",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.user": {
+ "name": "user",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(64)",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "email_verified": {
+ "name": "email_verified",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "image": {
+ "name": "image",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "password": {
+ "name": "password",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "provider": {
+ "name": "provider",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'credentials'"
+ },
+ "home_board_id": {
+ "name": "home_board_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "mobile_home_board_id": {
+ "name": "mobile_home_board_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "default_search_engine_id": {
+ "name": "default_search_engine_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "open_search_in_new_tab": {
+ "name": "open_search_in_new_tab",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "ddg_bangs": {
+ "name": "ddg_bangs",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "color_scheme": {
+ "name": "color_scheme",
+ "type": "varchar(5)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'dark'"
+ },
+ "first_day_of_week": {
+ "name": "first_day_of_week",
+ "type": "smallint",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 1
+ },
+ "ping_icons_enabled": {
+ "name": "ping_icons_enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "completed_manage_tour": {
+ "name": "completed_manage_tour",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "completed_board_tour": {
+ "name": "completed_board_tour",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "user_home_board_id_board_id_fk": {
+ "name": "user_home_board_id_board_id_fk",
+ "tableFrom": "user",
+ "tableTo": "board",
+ "columnsFrom": ["home_board_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "user_mobile_home_board_id_board_id_fk": {
+ "name": "user_mobile_home_board_id_board_id_fk",
+ "tableFrom": "user",
+ "tableTo": "board",
+ "columnsFrom": ["mobile_home_board_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "user_default_search_engine_id_search_engine_id_fk": {
+ "name": "user_default_search_engine_id_search_engine_id_fk",
+ "tableFrom": "user",
+ "tableTo": "search_engine",
+ "columnsFrom": ["default_search_engine_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.verificationToken": {
+ "name": "verificationToken",
+ "schema": "",
+ "columns": {
+ "identifier": {
+ "name": "identifier",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "token": {
+ "name": "token",
+ "type": "varchar(512)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires": {
+ "name": "expires",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "verificationToken_identifier_token_pk": {
+ "name": "verificationToken_identifier_token_pk",
+ "columns": ["identifier", "token"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.trusted_certificate_hostname": {
+ "name": "trusted_certificate_hostname",
+ "schema": "",
+ "columns": {
+ "hostname": {
+ "name": "hostname",
+ "type": "varchar(256)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "thumbprint": {
+ "name": "thumbprint",
+ "type": "varchar(128)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "certificate": {
+ "name": "certificate",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "trusted_certificate_hostname_hostname_thumbprint_pk": {
+ "name": "trusted_certificate_hostname_hostname_thumbprint_pk",
+ "columns": ["hostname", "thumbprint"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ }
+ },
+ "enums": {},
+ "schemas": {},
+ "sequences": {},
+ "roles": {},
+ "policies": {},
+ "views": {},
+ "_meta": {
+ "columns": {},
+ "schemas": {},
+ "tables": {}
+ }
+}
diff --git a/packages/db/migrations/postgresql/meta/_journal.json b/packages/db/migrations/postgresql/meta/_journal.json
index d0a3f7683..b6ecf835f 100644
--- a/packages/db/migrations/postgresql/meta/_journal.json
+++ b/packages/db/migrations/postgresql/meta/_journal.json
@@ -36,6 +36,13 @@
"when": 1775400000000,
"tag": "0004_add_user_ddg_bangs",
"breakpoints": true
+ },
+ {
+ "idx": 5,
+ "version": "7",
+ "when": 1779199097716,
+ "tag": "0005_add_user_onboarding_tour_status",
+ "breakpoints": true
}
]
}
diff --git a/packages/db/migrations/seed.ts b/packages/db/migrations/seed.ts
index a2dec94af..a2f19e59f 100644
--- a/packages/db/migrations/seed.ts
+++ b/packages/db/migrations/seed.ts
@@ -1,29 +1,38 @@
+import SuperJSON from "superjson";
+
import { createId, objectKeys } from "@homarr/common";
import {
createDocumentationLink,
credentialsAdminGroup,
+ defaultBookmarkApps,
everyoneGroup,
getIntegrationDefaultUrl,
getIntegrationName,
integrationDefs,
integrationKinds,
} from "@homarr/definitions";
+import type { WidgetKind } from "@homarr/definitions";
import { defaultServerSettings, defaultServerSettingsKeys } from "@homarr/server-settings";
import type { Database } from "..";
import { eq } from "..";
-import { getMaxGroupPositionAsync } from "../queries";
+import { getMaxGroupPositionAsync, placeAllWidgetsAsync } from "../queries";
import {
getServerSettingByKeyAsync,
insertServerSettingByKeyAsync,
updateServerSettingByKeyAsync,
} from "../queries/server-setting";
+
import {
+ apps,
boards,
groupMembers,
groupPermissions,
groups,
+ integrationItems,
integrations,
+ itemLayouts,
+ items,
layouts,
onboarding,
searchEngines,
@@ -32,14 +41,26 @@ import {
} from "../schema";
import type { Integration } from "../schema";
+const isTruthyEnv = (value: string | undefined) =>
+ ["1", "yes", "t", "true"].includes((value ?? "").toLowerCase());
+
export const seedDataAsync = async (db: Database) => {
+ if (isTruthyEnv(process.env.UNSAFE_ENABLE_MOCK_INTEGRATION)) {
+ console.warn(
+ "UNSAFE_ENABLE_MOCK_INTEGRATION is enabled: mock integration is available in the UI. Disable by setting UNSAFE_ENABLE_MOCK_INTEGRATION=false.",
+ );
+ }
+
await seedEveryoneGroupAsync(db);
await seedOnboardingAsync(db);
await seedServerSettingsAsync(db);
await seedDefaultSearchEnginesAsync(db);
await seedDefaultIntegrationsAsync(db);
+ await seedDefaultAppsAsync(db);
+ await seedDefaultBoardAsync(db);
+ await seedBoardWidgetsAsync(db);
- if (process.env.DEMO_MODE === "true") {
+ if (isTruthyEnv(process.env.DEMO_MODE)) {
await seedDemoUserAsync(db);
}
};
@@ -209,6 +230,192 @@ const seedDefaultIntegrationsAsync = async (db: Database) => {
console.log(`Created ${createdCount} default integrations through seeding process`);
};
+const seedDefaultAppsAsync = async (db: Database) => {
+ const existingApps = await db.$count(apps);
+ if (existingApps > 0) {
+ console.log("Skipping seeding of default apps as some already exist");
+ return;
+ }
+
+ for (const app of defaultBookmarkApps) {
+ await db.insert(apps).values({
+ id: createId(),
+ name: app.name,
+ iconUrl: app.iconUrl,
+ href: app.href,
+ });
+ }
+ console.log(`Created ${defaultBookmarkApps.length} default apps through seeding process`);
+};
+
+const seedDefaultBoardAsync = async (db: Database) => {
+ const existingBoard = await db.query.boards.findFirst();
+
+ if (existingBoard) {
+ console.log("Skipping seeding of default board as one already exists");
+ return;
+ }
+
+ const boardId = createId();
+ await db.insert(boards).values({
+ id: boardId,
+ name: "dashboard",
+ isPublic: false,
+ });
+ await db.insert(sections).values({
+ id: createId(),
+ kind: "empty",
+ xOffset: 0,
+ yOffset: 0,
+ boardId,
+ });
+ await db.insert(layouts).values({
+ id: createId(),
+ name: "Base",
+ columnCount: 10,
+ breakpoint: 0,
+ boardId,
+ });
+
+ const everyoneGroupRow = await db.query.groups.findFirst({
+ where: eq(groups.name, everyoneGroup),
+ });
+ if (everyoneGroupRow) {
+ await db.update(groups).set({ homeBoardId: boardId }).where(eq(groups.id, everyoneGroupRow.id));
+ console.log("Set default board as home board for everyone group");
+ }
+
+ console.log("Created default board 'dashboard' through seed");
+};
+
+interface DemoWidget {
+ kind: WidgetKind;
+ width: number;
+ height: number;
+ needsIntegration: boolean;
+ options?: Record;
+}
+
+const demoApps = [
+ {
+ name: "Sonarr",
+ iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/sonarr.svg",
+ href: "https://sonarr.tv",
+ },
+ {
+ name: "Radarr",
+ iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/radarr.svg",
+ href: "https://radarr.video",
+ },
+ {
+ name: "Plex",
+ iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/plex.svg",
+ href: "https://plex.tv",
+ },
+ {
+ name: "Jellyfin",
+ iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/jellyfin.svg",
+ href: "https://jellyfin.org",
+ },
+ {
+ name: "Proxmox",
+ iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/proxmox.svg",
+ href: "https://proxmox.com",
+ },
+ {
+ name: "Pi-hole",
+ iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/pi-hole.svg",
+ href: "https://pi-hole.net",
+ },
+ {
+ name: "Grafana",
+ iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/grafana.svg",
+ href: "https://grafana.com",
+ },
+ {
+ name: "Portainer",
+ iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/portainer.svg",
+ href: "https://portainer.io",
+ },
+ {
+ name: "Home Assistant",
+ iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/home-assistant.svg",
+ href: "https://home-assistant.io",
+ },
+ {
+ name: "Nextcloud",
+ iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/nextcloud.svg",
+ href: "https://nextcloud.com",
+ },
+ {
+ name: "qBittorrent",
+ iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/qbittorrent.svg",
+ href: "https://qbittorrent.org",
+ },
+ {
+ name: "Overseerr",
+ iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/overseerr.svg",
+ href: "https://overseerr.dev",
+ },
+] as const;
+
+const buildDemoWidgets = (appIds: string[]): DemoWidget[] => [
+ // Row 1: calendar + downloads + clock = 12
+ { kind: "calendar", width: 5, height: 3, needsIntegration: true },
+ { kind: "downloads", width: 5, height: 3, needsIntegration: true },
+ { kind: "clock", width: 2, height: 1, needsIntegration: false },
+ // Row 2: healthMonitoring + bookmarks = 12
+ { kind: "healthMonitoring", width: 6, height: 3, needsIntegration: true },
+ {
+ kind: "bookmarks",
+ width: 6,
+ height: 3,
+ needsIntegration: false,
+ options: { title: "Homelab", items: appIds, layout: "grid", openNewTab: true },
+ },
+ // Row 3: mediaServer + mediaTranscoding + dnsHoleSummary = 12
+ { kind: "mediaServer", width: 3, height: 2, needsIntegration: true },
+ { kind: "mediaTranscoding", width: 3, height: 2, needsIntegration: true },
+ { kind: "dnsHoleSummary", width: 6, height: 2, needsIntegration: true },
+ // Row 4: dnsHoleControls + mediaReleases + notifications = 12
+ { kind: "dnsHoleControls", width: 2, height: 1, needsIntegration: true },
+ { kind: "mediaReleases", width: 4, height: 2, needsIntegration: true },
+ { kind: "notifications", width: 6, height: 2, needsIntegration: true },
+ // Row 5: requestList + requestStats + indexerManager = 12
+ { kind: "mediaRequests-requestList", width: 4, height: 3, needsIntegration: true },
+ { kind: "mediaRequests-requestStats", width: 4, height: 2, needsIntegration: true },
+ { kind: "indexerManager", width: 4, height: 2, needsIntegration: true },
+ // Row 6: networkControllerSummary + networkControllerStatus + rssFeed = 12
+ { kind: "networkControllerSummary", width: 3, height: 2, needsIntegration: true },
+ { kind: "networkControllerStatus", width: 3, height: 2, needsIntegration: true },
+ {
+ kind: "rssFeed",
+ width: 6,
+ height: 2,
+ needsIntegration: false,
+ options: {
+ feedUrls: ["https://selfh.st/rss/", "https://hnrss.org/newest?q=self-hosted"],
+ maximumAmountPosts: 20,
+ textLinesClamp: 2,
+ hideDescription: false,
+ },
+ },
+ // Row 7-8: app widgets (2x1 each, 6 per row = 12)
+ ...appIds.map(
+ (appId): DemoWidget => ({
+ kind: "app",
+ width: 2,
+ height: 1,
+ needsIntegration: false,
+ options: { appId, openInNewTab: true, showTitle: true, pingEnabled: false },
+ }),
+ ),
+ // Row 9: notebook + dockerContainers + weather = 12
+ { kind: "notebook", width: 4, height: 4, needsIntegration: false },
+ { kind: "dockerContainers", width: 6, height: 2, needsIntegration: false },
+ { kind: "weather", width: 2, height: 1, needsIntegration: false },
+];
+
const seedDemoUserAsync = async (db: Database) => {
const existingUser = await db.query.users.findFirst({
where: eq(users.name, "demo"),
@@ -225,7 +432,7 @@ const seedDemoUserAsync = async (db: Database) => {
id: userId,
name: "demo",
email: "demo@example.com",
- password: "$2b$10$c5AaQb0oj32/bui5Z55PyOb7pY.iEkXYqgdD8MjS9LjSjLMeOkZu2",
+ password: "$2b$10$odRXt5e95kSQV5Axmk/FeO6GVOxuRQQ8NnRcBA78Wg4V3kZxPY68u",
});
const maxPosition = await getMaxGroupPositionAsync(db);
@@ -250,6 +457,27 @@ const seedDemoUserAsync = async (db: Database) => {
previousStep: "settings",
});
+ const integrationId = createId();
+ await db.insert(integrations).values({
+ id: integrationId,
+ name: "Demo Integration",
+ url: "https://demo.homarr.dev",
+ kind: "mock",
+ appId: null,
+ });
+
+ const appIds: string[] = [];
+ for (const app of demoApps) {
+ const appId = createId();
+ appIds.push(appId);
+ await db.insert(apps).values({
+ id: appId,
+ name: app.name,
+ iconUrl: app.iconUrl,
+ href: app.href,
+ });
+ }
+
const boardId = createId();
await db.insert(boards).values({
id: boardId,
@@ -257,23 +485,93 @@ const seedDemoUserAsync = async (db: Database) => {
isPublic: false,
creatorId: userId,
});
+
+ const sectionId = createId();
await db.insert(sections).values({
- id: createId(),
+ id: sectionId,
kind: "empty",
xOffset: 0,
yOffset: 0,
boardId,
});
+
+ const layoutId = createId();
await db.insert(layouts).values({
- id: createId(),
+ id: layoutId,
name: "Base",
columnCount: 12,
breakpoint: 0,
boardId,
});
+
+ const demoWidgets = buildDemoWidgets(appIds);
+ let xOffset = 0;
+ let yOffset = 0;
+ let rowMaxHeight = 0;
+ for (const widget of demoWidgets) {
+ if (xOffset + widget.width > 12) {
+ xOffset = 0;
+ yOffset += rowMaxHeight;
+ rowMaxHeight = 0;
+ }
+ rowMaxHeight = Math.max(rowMaxHeight, widget.height);
+ const itemId = createId();
+ await db.insert(items).values({
+ id: itemId,
+ boardId,
+ kind: widget.kind,
+ ...(widget.options ? { options: SuperJSON.stringify(widget.options) } : {}),
+ });
+ await db.insert(itemLayouts).values({
+ itemId,
+ sectionId,
+ layoutId,
+ xOffset,
+ yOffset,
+ width: widget.width,
+ height: widget.height,
+ });
+ if (widget.needsIntegration) {
+ await db.insert(integrationItems).values({
+ itemId,
+ integrationId,
+ });
+ }
+ xOffset += widget.width;
+ }
+
await db.update(users).set({ homeBoardId: boardId }).where(eq(users.id, userId));
console.log(
- "Your instance has enabled demo mode. Homarr therefore created example data for your demo. You can disable demo mode anytime by setting the env 'DEMO_MODE' to 'false' or unset it.",
+ "Demo mode enabled: created demo user, mock integration, and sample board with widgets. Disable by setting DEMO_MODE=false.",
);
};
+
+const seedBoardWidgetsAsync = async (db: Database) => {
+ const existingItems = await db.$count(items);
+ if (existingItems > 0) {
+ console.log("Skipping seeding of board widgets as some already exist");
+ return;
+ }
+
+ const board = await db.query.boards.findFirst({
+ with: { sections: true, layouts: true },
+ });
+ if (!board) return;
+
+ const section = board.sections.find((sec) => sec.kind === "empty");
+ const layout = board.layouts[0];
+ if (!section || !layout) return;
+
+ const allIntegrations = await db.query.integrations.findMany();
+ const allApps = await db.query.apps.findMany();
+
+ const count = await placeAllWidgetsAsync(
+ db,
+ { boardId: board.id, sectionId: section.id, layoutId: layout.id, columnCount: layout.columnCount },
+ allIntegrations,
+ allApps,
+ );
+
+ console.log(`Placed ${count} widgets on board`);
+};
diff --git a/packages/db/migrations/sqlite/0037_add_user_onboarding_tour_status.sql b/packages/db/migrations/sqlite/0037_add_user_onboarding_tour_status.sql
new file mode 100644
index 000000000..292ce4ece
--- /dev/null
+++ b/packages/db/migrations/sqlite/0037_add_user_onboarding_tour_status.sql
@@ -0,0 +1,2 @@
+ALTER TABLE `user` ADD `completed_manage_tour` integer DEFAULT false NOT NULL;--> statement-breakpoint
+ALTER TABLE `user` ADD `completed_board_tour` integer DEFAULT false NOT NULL;
\ No newline at end of file
diff --git a/packages/db/migrations/sqlite/meta/0037_snapshot.json b/packages/db/migrations/sqlite/meta/0037_snapshot.json
new file mode 100644
index 000000000..74827789c
--- /dev/null
+++ b/packages/db/migrations/sqlite/meta/0037_snapshot.json
@@ -0,0 +1,2035 @@
+{
+ "version": "6",
+ "dialect": "sqlite",
+ "id": "f30b6535-b4d5-4501-8370-7a7605e28bbd",
+ "prevId": "ddg-bangs-sqlite-0036",
+ "tables": {
+ "account": {
+ "name": "account",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "provider": {
+ "name": "provider",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "provider_account_id": {
+ "name": "provider_account_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "refresh_token": {
+ "name": "refresh_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "access_token": {
+ "name": "access_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "token_type": {
+ "name": "token_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "scope": {
+ "name": "scope",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "id_token": {
+ "name": "id_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "session_state": {
+ "name": "session_state",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "userId_idx": {
+ "name": "userId_idx",
+ "columns": ["user_id"],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "account_user_id_user_id_fk": {
+ "name": "account_user_id_user_id_fk",
+ "tableFrom": "account",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "account_provider_provider_account_id_pk": {
+ "columns": ["provider", "provider_account_id"],
+ "name": "account_provider_provider_account_id_pk"
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "apiKey": {
+ "name": "apiKey",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "api_key": {
+ "name": "api_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "apiKey_user_id_user_id_fk": {
+ "name": "apiKey_user_id_user_id_fk",
+ "tableFrom": "apiKey",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "app": {
+ "name": "app",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "icon_url": {
+ "name": "icon_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "href": {
+ "name": "href",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "ping_url": {
+ "name": "ping_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "boardGroupPermission": {
+ "name": "boardGroupPermission",
+ "columns": {
+ "board_id": {
+ "name": "board_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "group_id": {
+ "name": "group_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "permission": {
+ "name": "permission",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "boardGroupPermission_board_id_board_id_fk": {
+ "name": "boardGroupPermission_board_id_board_id_fk",
+ "tableFrom": "boardGroupPermission",
+ "tableTo": "board",
+ "columnsFrom": ["board_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "boardGroupPermission_group_id_group_id_fk": {
+ "name": "boardGroupPermission_group_id_group_id_fk",
+ "tableFrom": "boardGroupPermission",
+ "tableTo": "group",
+ "columnsFrom": ["group_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "boardGroupPermission_board_id_group_id_permission_pk": {
+ "columns": ["board_id", "group_id", "permission"],
+ "name": "boardGroupPermission_board_id_group_id_permission_pk"
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "boardUserPermission": {
+ "name": "boardUserPermission",
+ "columns": {
+ "board_id": {
+ "name": "board_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "permission": {
+ "name": "permission",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "boardUserPermission_board_id_board_id_fk": {
+ "name": "boardUserPermission_board_id_board_id_fk",
+ "tableFrom": "boardUserPermission",
+ "tableTo": "board",
+ "columnsFrom": ["board_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "boardUserPermission_user_id_user_id_fk": {
+ "name": "boardUserPermission_user_id_user_id_fk",
+ "tableFrom": "boardUserPermission",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "boardUserPermission_board_id_user_id_permission_pk": {
+ "columns": ["board_id", "user_id", "permission"],
+ "name": "boardUserPermission_board_id_user_id_permission_pk"
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "board": {
+ "name": "board",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "is_public": {
+ "name": "is_public",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ },
+ "creator_id": {
+ "name": "creator_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "page_title": {
+ "name": "page_title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "meta_title": {
+ "name": "meta_title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "logo_image_url": {
+ "name": "logo_image_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "favicon_image_url": {
+ "name": "favicon_image_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "background_image_url": {
+ "name": "background_image_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "background_image_attachment": {
+ "name": "background_image_attachment",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'fixed'"
+ },
+ "background_image_repeat": {
+ "name": "background_image_repeat",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'no-repeat'"
+ },
+ "background_image_size": {
+ "name": "background_image_size",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'cover'"
+ },
+ "primary_color": {
+ "name": "primary_color",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'#fa5252'"
+ },
+ "secondary_color": {
+ "name": "secondary_color",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'#fd7e14'"
+ },
+ "opacity": {
+ "name": "opacity",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": 100
+ },
+ "custom_css": {
+ "name": "custom_css",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "icon_color": {
+ "name": "icon_color",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "item_radius": {
+ "name": "item_radius",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'lg'"
+ },
+ "disable_status": {
+ "name": "disable_status",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ }
+ },
+ "indexes": {
+ "board_name_unique": {
+ "name": "board_name_unique",
+ "columns": ["name"],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {
+ "board_creator_id_user_id_fk": {
+ "name": "board_creator_id_user_id_fk",
+ "tableFrom": "board",
+ "tableTo": "user",
+ "columnsFrom": ["creator_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "cron_job_configuration": {
+ "name": "cron_job_configuration",
+ "columns": {
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "cron_expression": {
+ "name": "cron_expression",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "is_enabled": {
+ "name": "is_enabled",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "groupMember": {
+ "name": "groupMember",
+ "columns": {
+ "group_id": {
+ "name": "group_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "groupMember_group_id_group_id_fk": {
+ "name": "groupMember_group_id_group_id_fk",
+ "tableFrom": "groupMember",
+ "tableTo": "group",
+ "columnsFrom": ["group_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "groupMember_user_id_user_id_fk": {
+ "name": "groupMember_user_id_user_id_fk",
+ "tableFrom": "groupMember",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "groupMember_group_id_user_id_pk": {
+ "columns": ["group_id", "user_id"],
+ "name": "groupMember_group_id_user_id_pk"
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "groupPermission": {
+ "name": "groupPermission",
+ "columns": {
+ "group_id": {
+ "name": "group_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "permission": {
+ "name": "permission",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "groupPermission_group_id_group_id_fk": {
+ "name": "groupPermission_group_id_group_id_fk",
+ "tableFrom": "groupPermission",
+ "tableTo": "group",
+ "columnsFrom": ["group_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "group": {
+ "name": "group",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "owner_id": {
+ "name": "owner_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "home_board_id": {
+ "name": "home_board_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "mobile_home_board_id": {
+ "name": "mobile_home_board_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "position": {
+ "name": "position",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "group_name_unique": {
+ "name": "group_name_unique",
+ "columns": ["name"],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {
+ "group_owner_id_user_id_fk": {
+ "name": "group_owner_id_user_id_fk",
+ "tableFrom": "group",
+ "tableTo": "user",
+ "columnsFrom": ["owner_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "group_home_board_id_board_id_fk": {
+ "name": "group_home_board_id_board_id_fk",
+ "tableFrom": "group",
+ "tableTo": "board",
+ "columnsFrom": ["home_board_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "group_mobile_home_board_id_board_id_fk": {
+ "name": "group_mobile_home_board_id_board_id_fk",
+ "tableFrom": "group",
+ "tableTo": "board",
+ "columnsFrom": ["mobile_home_board_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "iconRepository": {
+ "name": "iconRepository",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "slug": {
+ "name": "slug",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "icon": {
+ "name": "icon",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "url": {
+ "name": "url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "checksum": {
+ "name": "checksum",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "icon_repository_id": {
+ "name": "icon_repository_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "icon_icon_repository_id_iconRepository_id_fk": {
+ "name": "icon_icon_repository_id_iconRepository_id_fk",
+ "tableFrom": "icon",
+ "tableTo": "iconRepository",
+ "columnsFrom": ["icon_repository_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "integrationGroupPermissions": {
+ "name": "integrationGroupPermissions",
+ "columns": {
+ "integration_id": {
+ "name": "integration_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "group_id": {
+ "name": "group_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "permission": {
+ "name": "permission",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "integrationGroupPermissions_integration_id_integration_id_fk": {
+ "name": "integrationGroupPermissions_integration_id_integration_id_fk",
+ "tableFrom": "integrationGroupPermissions",
+ "tableTo": "integration",
+ "columnsFrom": ["integration_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "integrationGroupPermissions_group_id_group_id_fk": {
+ "name": "integrationGroupPermissions_group_id_group_id_fk",
+ "tableFrom": "integrationGroupPermissions",
+ "tableTo": "group",
+ "columnsFrom": ["group_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "integrationGroupPermissions_integration_id_group_id_permission_pk": {
+ "columns": ["integration_id", "group_id", "permission"],
+ "name": "integrationGroupPermissions_integration_id_group_id_permission_pk"
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "integration_item": {
+ "name": "integration_item",
+ "columns": {
+ "item_id": {
+ "name": "item_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "integration_id": {
+ "name": "integration_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "integration_item_item_id_item_id_fk": {
+ "name": "integration_item_item_id_item_id_fk",
+ "tableFrom": "integration_item",
+ "tableTo": "item",
+ "columnsFrom": ["item_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "integration_item_integration_id_integration_id_fk": {
+ "name": "integration_item_integration_id_integration_id_fk",
+ "tableFrom": "integration_item",
+ "tableTo": "integration",
+ "columnsFrom": ["integration_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "integration_item_item_id_integration_id_pk": {
+ "columns": ["item_id", "integration_id"],
+ "name": "integration_item_item_id_integration_id_pk"
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "integrationSecret": {
+ "name": "integrationSecret",
+ "columns": {
+ "kind": {
+ "name": "kind",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "value": {
+ "name": "value",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "integration_id": {
+ "name": "integration_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "integration_secret__kind_idx": {
+ "name": "integration_secret__kind_idx",
+ "columns": ["kind"],
+ "isUnique": false
+ },
+ "integration_secret__updated_at_idx": {
+ "name": "integration_secret__updated_at_idx",
+ "columns": ["updated_at"],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "integrationSecret_integration_id_integration_id_fk": {
+ "name": "integrationSecret_integration_id_integration_id_fk",
+ "tableFrom": "integrationSecret",
+ "tableTo": "integration",
+ "columnsFrom": ["integration_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "integrationSecret_integration_id_kind_pk": {
+ "columns": ["integration_id", "kind"],
+ "name": "integrationSecret_integration_id_kind_pk"
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "integrationUserPermission": {
+ "name": "integrationUserPermission",
+ "columns": {
+ "integration_id": {
+ "name": "integration_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "permission": {
+ "name": "permission",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "integrationUserPermission_integration_id_integration_id_fk": {
+ "name": "integrationUserPermission_integration_id_integration_id_fk",
+ "tableFrom": "integrationUserPermission",
+ "tableTo": "integration",
+ "columnsFrom": ["integration_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "integrationUserPermission_user_id_user_id_fk": {
+ "name": "integrationUserPermission_user_id_user_id_fk",
+ "tableFrom": "integrationUserPermission",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "integrationUserPermission_integration_id_user_id_permission_pk": {
+ "columns": ["integration_id", "user_id", "permission"],
+ "name": "integrationUserPermission_integration_id_user_id_permission_pk"
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "integration": {
+ "name": "integration",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "url": {
+ "name": "url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "kind": {
+ "name": "kind",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "app_id": {
+ "name": "app_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "integration__kind_idx": {
+ "name": "integration__kind_idx",
+ "columns": ["kind"],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "integration_app_id_app_id_fk": {
+ "name": "integration_app_id_app_id_fk",
+ "tableFrom": "integration",
+ "tableTo": "app",
+ "columnsFrom": ["app_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "invite": {
+ "name": "invite",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "token": {
+ "name": "token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "expiration_date": {
+ "name": "expiration_date",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "creator_id": {
+ "name": "creator_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "invite_token_unique": {
+ "name": "invite_token_unique",
+ "columns": ["token"],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {
+ "invite_creator_id_user_id_fk": {
+ "name": "invite_creator_id_user_id_fk",
+ "tableFrom": "invite",
+ "tableTo": "user",
+ "columnsFrom": ["creator_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "item_layout": {
+ "name": "item_layout",
+ "columns": {
+ "item_id": {
+ "name": "item_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "section_id": {
+ "name": "section_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "layout_id": {
+ "name": "layout_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "x_offset": {
+ "name": "x_offset",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "y_offset": {
+ "name": "y_offset",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "width": {
+ "name": "width",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "height": {
+ "name": "height",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "item_layout_item_id_item_id_fk": {
+ "name": "item_layout_item_id_item_id_fk",
+ "tableFrom": "item_layout",
+ "tableTo": "item",
+ "columnsFrom": ["item_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "item_layout_section_id_section_id_fk": {
+ "name": "item_layout_section_id_section_id_fk",
+ "tableFrom": "item_layout",
+ "tableTo": "section",
+ "columnsFrom": ["section_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "item_layout_layout_id_layout_id_fk": {
+ "name": "item_layout_layout_id_layout_id_fk",
+ "tableFrom": "item_layout",
+ "tableTo": "layout",
+ "columnsFrom": ["layout_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "item_layout_item_id_section_id_layout_id_pk": {
+ "columns": ["item_id", "section_id", "layout_id"],
+ "name": "item_layout_item_id_section_id_layout_id_pk"
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "item": {
+ "name": "item",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "board_id": {
+ "name": "board_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "kind": {
+ "name": "kind",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "options": {
+ "name": "options",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'{\"json\": {}}'"
+ },
+ "advanced_options": {
+ "name": "advanced_options",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'{\"json\": {}}'"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "item_board_id_board_id_fk": {
+ "name": "item_board_id_board_id_fk",
+ "tableFrom": "item",
+ "tableTo": "board",
+ "columnsFrom": ["board_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "layout": {
+ "name": "layout",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "board_id": {
+ "name": "board_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "column_count": {
+ "name": "column_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "breakpoint": {
+ "name": "breakpoint",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": 0
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "layout_board_id_board_id_fk": {
+ "name": "layout_board_id_board_id_fk",
+ "tableFrom": "layout",
+ "tableTo": "board",
+ "columnsFrom": ["board_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "media": {
+ "name": "media",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "content": {
+ "name": "content",
+ "type": "blob",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "content_type": {
+ "name": "content_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "size": {
+ "name": "size",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch())"
+ },
+ "creator_id": {
+ "name": "creator_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "media_creator_id_user_id_fk": {
+ "name": "media_creator_id_user_id_fk",
+ "tableFrom": "media",
+ "tableTo": "user",
+ "columnsFrom": ["creator_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "onboarding": {
+ "name": "onboarding",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "step": {
+ "name": "step",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "previous_step": {
+ "name": "previous_step",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "search_engine": {
+ "name": "search_engine",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "icon_url": {
+ "name": "icon_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "short": {
+ "name": "short",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "url_template": {
+ "name": "url_template",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'generic'"
+ },
+ "integration_id": {
+ "name": "integration_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "search_engine_short_unique": {
+ "name": "search_engine_short_unique",
+ "columns": ["short"],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {
+ "search_engine_integration_id_integration_id_fk": {
+ "name": "search_engine_integration_id_integration_id_fk",
+ "tableFrom": "search_engine",
+ "tableTo": "integration",
+ "columnsFrom": ["integration_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "section_collapse_state": {
+ "name": "section_collapse_state",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "section_id": {
+ "name": "section_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "collapsed": {
+ "name": "collapsed",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "section_collapse_state_user_id_user_id_fk": {
+ "name": "section_collapse_state_user_id_user_id_fk",
+ "tableFrom": "section_collapse_state",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "section_collapse_state_section_id_section_id_fk": {
+ "name": "section_collapse_state_section_id_section_id_fk",
+ "tableFrom": "section_collapse_state",
+ "tableTo": "section",
+ "columnsFrom": ["section_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "section_collapse_state_user_id_section_id_pk": {
+ "columns": ["user_id", "section_id"],
+ "name": "section_collapse_state_user_id_section_id_pk"
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "section_layout": {
+ "name": "section_layout",
+ "columns": {
+ "section_id": {
+ "name": "section_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "layout_id": {
+ "name": "layout_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "parent_section_id": {
+ "name": "parent_section_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "x_offset": {
+ "name": "x_offset",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "y_offset": {
+ "name": "y_offset",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "width": {
+ "name": "width",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "height": {
+ "name": "height",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "section_layout_section_id_section_id_fk": {
+ "name": "section_layout_section_id_section_id_fk",
+ "tableFrom": "section_layout",
+ "tableTo": "section",
+ "columnsFrom": ["section_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "section_layout_layout_id_layout_id_fk": {
+ "name": "section_layout_layout_id_layout_id_fk",
+ "tableFrom": "section_layout",
+ "tableTo": "layout",
+ "columnsFrom": ["layout_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "section_layout_parent_section_id_section_id_fk": {
+ "name": "section_layout_parent_section_id_section_id_fk",
+ "tableFrom": "section_layout",
+ "tableTo": "section",
+ "columnsFrom": ["parent_section_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "section_layout_section_id_layout_id_pk": {
+ "columns": ["section_id", "layout_id"],
+ "name": "section_layout_section_id_layout_id_pk"
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "section": {
+ "name": "section",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "board_id": {
+ "name": "board_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "kind": {
+ "name": "kind",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "x_offset": {
+ "name": "x_offset",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "y_offset": {
+ "name": "y_offset",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "options": {
+ "name": "options",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'{\"json\": {}}'"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "section_board_id_board_id_fk": {
+ "name": "section_board_id_board_id_fk",
+ "tableFrom": "section",
+ "tableTo": "board",
+ "columnsFrom": ["board_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "serverSetting": {
+ "name": "serverSetting",
+ "columns": {
+ "setting_key": {
+ "name": "setting_key",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "value": {
+ "name": "value",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'{\"json\": {}}'"
+ }
+ },
+ "indexes": {
+ "serverSetting_settingKey_unique": {
+ "name": "serverSetting_settingKey_unique",
+ "columns": ["setting_key"],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "session": {
+ "name": "session",
+ "columns": {
+ "session_token": {
+ "name": "session_token",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "expires": {
+ "name": "expires",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "user_id_idx": {
+ "name": "user_id_idx",
+ "columns": ["user_id"],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "session_user_id_user_id_fk": {
+ "name": "session_user_id_user_id_fk",
+ "tableFrom": "session",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "trusted_certificate_hostname": {
+ "name": "trusted_certificate_hostname",
+ "columns": {
+ "hostname": {
+ "name": "hostname",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "thumbprint": {
+ "name": "thumbprint",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "certificate": {
+ "name": "certificate",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "trusted_certificate_hostname_hostname_thumbprint_pk": {
+ "columns": ["hostname", "thumbprint"],
+ "name": "trusted_certificate_hostname_hostname_thumbprint_pk"
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "user": {
+ "name": "user",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "email_verified": {
+ "name": "email_verified",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "image": {
+ "name": "image",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "password": {
+ "name": "password",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "provider": {
+ "name": "provider",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'credentials'"
+ },
+ "home_board_id": {
+ "name": "home_board_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "mobile_home_board_id": {
+ "name": "mobile_home_board_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "default_search_engine_id": {
+ "name": "default_search_engine_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "open_search_in_new_tab": {
+ "name": "open_search_in_new_tab",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": true
+ },
+ "ddg_bangs": {
+ "name": "ddg_bangs",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": true
+ },
+ "color_scheme": {
+ "name": "color_scheme",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'dark'"
+ },
+ "first_day_of_week": {
+ "name": "first_day_of_week",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": 1
+ },
+ "ping_icons_enabled": {
+ "name": "ping_icons_enabled",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ },
+ "completed_manage_tour": {
+ "name": "completed_manage_tour",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ },
+ "completed_board_tour": {
+ "name": "completed_board_tour",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "user_home_board_id_board_id_fk": {
+ "name": "user_home_board_id_board_id_fk",
+ "tableFrom": "user",
+ "tableTo": "board",
+ "columnsFrom": ["home_board_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "user_mobile_home_board_id_board_id_fk": {
+ "name": "user_mobile_home_board_id_board_id_fk",
+ "tableFrom": "user",
+ "tableTo": "board",
+ "columnsFrom": ["mobile_home_board_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "user_default_search_engine_id_search_engine_id_fk": {
+ "name": "user_default_search_engine_id_search_engine_id_fk",
+ "tableFrom": "user",
+ "tableTo": "search_engine",
+ "columnsFrom": ["default_search_engine_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "verificationToken": {
+ "name": "verificationToken",
+ "columns": {
+ "identifier": {
+ "name": "identifier",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "token": {
+ "name": "token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "expires": {
+ "name": "expires",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "verificationToken_identifier_token_pk": {
+ "columns": ["identifier", "token"],
+ "name": "verificationToken_identifier_token_pk"
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ }
+ },
+ "views": {},
+ "enums": {},
+ "_meta": {
+ "schemas": {},
+ "tables": {},
+ "columns": {}
+ },
+ "internal": {
+ "indexes": {}
+ }
+}
diff --git a/packages/db/migrations/sqlite/meta/_journal.json b/packages/db/migrations/sqlite/meta/_journal.json
index 5543bbde0..2f4866c79 100644
--- a/packages/db/migrations/sqlite/meta/_journal.json
+++ b/packages/db/migrations/sqlite/meta/_journal.json
@@ -260,6 +260,13 @@
"when": 1775400000000,
"tag": "0036_add_user_ddg_bangs",
"breakpoints": true
+ },
+ {
+ "idx": 37,
+ "version": "6",
+ "when": 1779199046805,
+ "tag": "0037_add_user_onboarding_tour_status",
+ "breakpoints": true
}
]
}
diff --git a/packages/db/queries/index.ts b/packages/db/queries/index.ts
index 13e90d6eb..368d7eaa2 100644
--- a/packages/db/queries/index.ts
+++ b/packages/db/queries/index.ts
@@ -1,3 +1,4 @@
export * from "./item";
export * from "./server-setting";
export * from "./group";
+export * from "./widget-placement";
diff --git a/packages/db/queries/widget-placement.ts b/packages/db/queries/widget-placement.ts
new file mode 100644
index 000000000..1d0b529f9
--- /dev/null
+++ b/packages/db/queries/widget-placement.ts
@@ -0,0 +1,148 @@
+import superjson from "superjson";
+
+import { createId } from "@homarr/common";
+import {
+ defaultBookmarkApps,
+ defaultWidgetConfigs,
+ emptySuperJSON,
+ getWidgetKindsForIntegration,
+} from "@homarr/definitions";
+import type { WidgetKind } from "@homarr/definitions";
+
+import type { Database } from "..";
+import { integrationItems, itemLayouts, items } from "../schema";
+import type { Integration } from "../schema";
+
+const widgetConfigMap = new Map(defaultWidgetConfigs.map((config) => [config.kind, config]));
+
+interface BoardTarget {
+ boardId: string;
+ sectionId: string;
+ layoutId: string;
+ columnCount: number;
+}
+
+interface PlacementContext extends BoardTarget {
+ xOffset: number;
+ yOffset: number;
+ rowMaxHeight: number;
+}
+
+const placeWidgetAsync = async (
+ db: Database,
+ ctx: PlacementContext,
+ kind: WidgetKind,
+ linkedIntegrationIds: string[],
+ options?: string,
+ size?: { width: number; height: number },
+) => {
+ const itemWidth = size?.width ?? 2;
+ const itemHeight = size?.height ?? 2;
+
+ if (ctx.xOffset + itemWidth > ctx.columnCount) {
+ ctx.xOffset = 0;
+ ctx.yOffset += ctx.rowMaxHeight;
+ ctx.rowMaxHeight = 0;
+ }
+
+ const itemId = createId();
+ await db.insert(items).values({
+ id: itemId,
+ boardId: ctx.boardId,
+ kind,
+ options: options ?? emptySuperJSON,
+ advancedOptions: emptySuperJSON,
+ });
+
+ await db.insert(itemLayouts).values({
+ itemId,
+ sectionId: ctx.sectionId,
+ layoutId: ctx.layoutId,
+ xOffset: ctx.xOffset,
+ yOffset: ctx.yOffset,
+ width: itemWidth,
+ height: itemHeight,
+ });
+
+ for (const integrationId of linkedIntegrationIds) {
+ await db.insert(integrationItems).values({ itemId, integrationId });
+ }
+
+ ctx.rowMaxHeight = Math.max(ctx.rowMaxHeight, itemHeight);
+ ctx.xOffset += itemWidth;
+};
+
+interface App {
+ id: string;
+ name: string;
+}
+
+export const placeAllWidgetsAsync = async (
+ db: Database,
+ target: BoardTarget,
+ allIntegrations: Integration[],
+ allApps: App[],
+) => {
+ const ctx: PlacementContext = {
+ ...target,
+ xOffset: 0,
+ yOffset: 0,
+ rowMaxHeight: 0,
+ };
+
+ const placedWidgets = new Set();
+
+ for (const integration of allIntegrations) {
+ for (const widgetKind of getWidgetKindsForIntegration(integration.kind)) {
+ if (placedWidgets.has(widgetKind)) continue;
+ const config = widgetConfigMap.get(widgetKind);
+ if (config?.skip) continue;
+ placedWidgets.add(widgetKind);
+
+ const matchingIds = allIntegrations
+ .filter((row) => getWidgetKindsForIntegration(row.kind).includes(widgetKind))
+ .map((row) => row.id);
+
+ const options = config?.options ? superjson.stringify(config.options) : undefined;
+ await placeWidgetAsync(
+ db,
+ ctx,
+ widgetKind,
+ matchingIds,
+ options,
+ config && { width: config.width, height: config.height },
+ );
+ }
+ }
+
+ for (const app of allApps) {
+ await placeWidgetAsync(
+ db,
+ ctx,
+ "app",
+ [],
+ superjson.stringify({ appId: app.id, openInNewTab: true, showTitle: true }),
+ { width: 1, height: 1 },
+ );
+ }
+
+ const bookmarkAppNames = new Set(defaultBookmarkApps.map((bookmark) => bookmark.name));
+ const bookmarkAppIds = allApps.filter((app) => bookmarkAppNames.has(app.name)).map((app) => app.id);
+
+ for (const config of defaultWidgetConfigs) {
+ if (config.skip || placedWidgets.has(config.kind)) continue;
+ placedWidgets.add(config.kind);
+
+ let options = config.options ? { ...config.options } : undefined;
+ if (config.kind === "bookmarks" && bookmarkAppIds.length > 0) {
+ options = { ...options, items: bookmarkAppIds };
+ }
+
+ await placeWidgetAsync(db, ctx, config.kind, [], options ? superjson.stringify(options) : undefined, {
+ width: config.width,
+ height: config.height,
+ });
+ }
+
+ return placedWidgets.size + allApps.length;
+};
diff --git a/packages/db/schema/mysql.ts b/packages/db/schema/mysql.ts
index 84c5325ed..7e2897c46 100644
--- a/packages/db/schema/mysql.ts
+++ b/packages/db/schema/mysql.ts
@@ -80,6 +80,8 @@ export const users = mysqlTable("user", {
colorScheme: varchar({ length: 5 }).$type().default("dark").notNull(),
firstDayOfWeek: tinyint().$type().default(1).notNull(), // Defaults to Monday
pingIconsEnabled: boolean().default(false).notNull(),
+ completedManageTour: boolean().default(false).notNull(),
+ completedBoardTour: boolean().default(false).notNull(),
});
export const accounts = mysqlTable(
diff --git a/packages/db/schema/postgresql.ts b/packages/db/schema/postgresql.ts
index 412916013..8ea445325 100644
--- a/packages/db/schema/postgresql.ts
+++ b/packages/db/schema/postgresql.ts
@@ -79,6 +79,8 @@ export const users = pgTable("user", {
colorScheme: varchar({ length: 5 }).$type().default("dark").notNull(),
firstDayOfWeek: smallint().$type().default(1).notNull(), // Defaults to Monday
pingIconsEnabled: boolean().default(false).notNull(),
+ completedManageTour: boolean().default(false).notNull(),
+ completedBoardTour: boolean().default(false).notNull(),
});
export const accounts = pgTable(
diff --git a/packages/db/schema/sqlite.ts b/packages/db/schema/sqlite.ts
index 50671db85..b4fe5bbe0 100644
--- a/packages/db/schema/sqlite.ts
+++ b/packages/db/schema/sqlite.ts
@@ -62,6 +62,8 @@ export const users = sqliteTable("user", {
colorScheme: text().$type().default("dark").notNull(),
firstDayOfWeek: int().$type().default(1).notNull(), // Defaults to Monday
pingIconsEnabled: int({ mode: "boolean" }).default(false).notNull(),
+ completedManageTour: int({ mode: "boolean" }).default(false).notNull(),
+ completedBoardTour: int({ mode: "boolean" }).default(false).notNull(),
});
export const accounts = sqliteTable(
diff --git a/packages/definitions/src/docker-integration-match.ts b/packages/definitions/src/docker-integration-match.ts
new file mode 100644
index 000000000..c85715808
--- /dev/null
+++ b/packages/definitions/src/docker-integration-match.ts
@@ -0,0 +1,90 @@
+import { objectKeys } from "@homarr/common";
+
+import { integrationDefs } from "./integration";
+import type { IntegrationKind } from "./integration";
+
+export const extractContainerImageName = (image: string): string => image.split("/").at(-1)?.split(":").at(0) ?? "";
+
+const extractIconSlug = (iconUrl: string): string => {
+ const filename = iconUrl.split("/").pop() ?? "";
+ return filename.replace(/\.(svg|png)$/, "").toLowerCase();
+};
+
+export const integrationIconSlugs: Record = Object.fromEntries(
+ objectKeys(integrationDefs).map((kind) => [kind, extractIconSlug(integrationDefs[kind].iconUrl)]),
+) as Record;
+
+const integrationAliases: Partial> = {
+ piHole: ["pihole", "pi-hole"],
+ adGuardHome: ["adguardhome", "adguard-home", "adguard"],
+ homeAssistant: ["homeassistant", "home-assistant", "hass"],
+ openmediavault: ["omv"],
+ qBittorrent: ["qbittorrent"],
+ sabNzbd: ["sabnzbd"],
+ nzbGet: ["nzbget"],
+ unifiController: ["unifi", "unifi-controller"],
+ jellyseerr: ["jellyseerr"],
+ overseerr: ["overseerr"],
+ dashDot: ["dashdot", "dash-dot", "dash."],
+ speedtestTracker: ["speedtest-tracker"],
+ searchCh: ["search-ch"],
+ gitHubContainerRegistry: ["ghcr"],
+ linuxServerIO: ["linuxserver"],
+ coolify: ["coolify"],
+ truenas: ["truenas"],
+};
+
+export const matchIntegrationKind = (search: string): IntegrationKind | null => {
+ const normalized = search.toLowerCase().trim();
+ if (!normalized) return null;
+
+ for (const kind of objectKeys(integrationDefs)) {
+ if (kind.toLowerCase() === normalized) return kind;
+ if (integrationDefs[kind].name.toLowerCase() === normalized) return kind;
+ }
+
+ for (const kind of objectKeys(integrationDefs)) {
+ if (integrationIconSlugs[kind] === normalized) return kind;
+ }
+
+ for (const [kind, aliases] of Object.entries(integrationAliases) as [IntegrationKind, readonly string[]][]) {
+ if (aliases.some((alias) => alias === normalized)) return kind;
+ }
+
+ for (const kind of objectKeys(integrationDefs)) {
+ if (normalized.includes(kind.toLowerCase())) return kind;
+ if (normalized.includes(integrationIconSlugs[kind])) return kind;
+ }
+
+ return null;
+};
+
+const notDockerDiscoverable = new Set([
+ "dockerHub",
+ "github",
+ "gitlab",
+ "npm",
+ "codeberg",
+ "linuxServerIO",
+ "gitHubContainerRegistry",
+ "quay",
+ "ical",
+ "searchCh",
+ "mock",
+]);
+
+interface ContainerMatchInput {
+ image: string;
+ name: string;
+}
+
+export const matchIntegrationKindFromContainer = (container: ContainerMatchInput): IntegrationKind | null => {
+ const imageName = extractContainerImageName(container.image);
+ const fromImage = matchIntegrationKind(imageName);
+ if (fromImage && !notDockerDiscoverable.has(fromImage)) return fromImage;
+
+ const fromName = matchIntegrationKind(container.name);
+ if (fromName && !notDockerDiscoverable.has(fromName)) return fromName;
+
+ return null;
+};
diff --git a/packages/definitions/src/docs/homarr-docs-sitemap.ts b/packages/definitions/src/docs/homarr-docs-sitemap.ts
index b97a34937..348bbf78d 100644
--- a/packages/definitions/src/docs/homarr-docs-sitemap.ts
+++ b/packages/definitions/src/docs/homarr-docs-sitemap.ts
@@ -157,6 +157,7 @@ export type HomarrDocumentationPath =
| "/docs/integrations/anchor"
| "/docs/integrations/aria2"
| "/docs/integrations/codeberg"
+ | "/docs/integrations/coolify"
| "/docs/integrations/dash-dot"
| "/docs/integrations/deluge"
| "/docs/integrations/docker-hub"
@@ -181,6 +182,7 @@ export type HomarrDocumentationPath =
| "/docs/integrations/open-media-vault"
| "/docs/integrations/opnsense"
| "/docs/integrations/overseerr"
+ | "/docs/integrations/peanut"
| "/docs/integrations/pi-hole"
| "/docs/integrations/plex"
| "/docs/integrations/prowlarr"
@@ -217,6 +219,7 @@ export type HomarrDocumentationPath =
| "/docs/widgets/bookmarks"
| "/docs/widgets/calendar"
| "/docs/widgets/clock"
+ | "/docs/widgets/coolify"
| "/docs/widgets/dns-hole-controls"
| "/docs/widgets/dns-hole-summary"
| "/docs/widgets/docker-containers"
@@ -247,6 +250,7 @@ export type HomarrDocumentationPath =
| "/docs/widgets/timetable"
| "/docs/widgets/tracearr"
| "/docs/widgets/umami"
+ | "/docs/widgets/ups"
| "/docs/widgets/video"
| "/docs/widgets/weather"
| ""
diff --git a/packages/definitions/src/index.ts b/packages/definitions/src/index.ts
index 3eb639ba6..eb81e4d18 100644
--- a/packages/definitions/src/index.ts
+++ b/packages/definitions/src/index.ts
@@ -12,5 +12,8 @@ export * from "./docs";
export * from "./cookie";
export * from "./search-engine";
export * from "./onboarding";
+export * from "./widget-integration-map";
export * from "./emptysuperjson";
export * from "./hotkeys";
+export * from "./docker-integration-match";
+export * from "./integration-url-template";
diff --git a/packages/definitions/src/integration-url-template.ts b/packages/definitions/src/integration-url-template.ts
new file mode 100644
index 000000000..bb8312c6e
--- /dev/null
+++ b/packages/definitions/src/integration-url-template.ts
@@ -0,0 +1,41 @@
+import { removeTrailingSlash } from "@homarr/common";
+
+import { integrationIconSlugs } from "./docker-integration-match";
+import type { IntegrationKind } from "./integration";
+import { getIntegrationDefaultPort } from "./integration";
+
+export type UrlTemplateMode = "hostPort" | "subdomain";
+
+const getSlugForKind = (kind: IntegrationKind): string => integrationIconSlugs[kind];
+
+const buildUrl = (slug: string, host: string, mode: UrlTemplateMode, port?: number): string => {
+ const modeBuilders: Record string> = {
+ subdomain: () => `https://${slug}.${host}`,
+ hostPort: () => (port ? `http://${host}:${port}` : `http://${host}`),
+ };
+ return modeBuilders[mode]();
+};
+
+export const buildIntegrationUrl = (
+ kind: IntegrationKind,
+ baseHost: string,
+ mode: UrlTemplateMode,
+ dockerPort?: number,
+): string => {
+ const host = removeTrailingSlash(baseHost.trim());
+ if (!host) return "";
+ const port = dockerPort ?? getIntegrationDefaultPort(kind);
+ return buildUrl(getSlugForKind(kind), host, mode, port ?? undefined);
+};
+
+export const buildAppUrl = (
+ containerName: string,
+ baseHost: string,
+ mode: UrlTemplateMode,
+ dockerPort?: number,
+): string => {
+ const host = removeTrailingSlash(baseHost.trim());
+ if (!host) return "";
+ const slug = containerName.toLowerCase().replace(/[^a-z0-9-]/g, "-");
+ return buildUrl(slug, host, mode, dockerPort);
+};
diff --git a/packages/definitions/src/integration.ts b/packages/definitions/src/integration.ts
index f90f81a38..f43d821bf 100644
--- a/packages/definitions/src/integration.ts
+++ b/packages/definitions/src/integration.ts
@@ -1,4 +1,4 @@
-import { objectKeys } from "@homarr/common";
+import { objectKeys, removeTrailingSlash } from "@homarr/common";
import type { AtLeastOneOf } from "@homarr/common/types";
import { createDocumentationLink } from "./docs";
@@ -27,7 +27,9 @@ interface integrationDefinition {
secretKinds: AtLeastOneOf; // at least one secret kind set is required
category: AtLeastOneOf;
documentationUrl: string | null;
- defaultUrl?: string; // optional default URL for the integration
+ defaultUrl?: string;
+ defaultPort?: number;
+ apiKeySettingsPath?: string;
}
export const integrationDefs = {
@@ -37,6 +39,8 @@ export const integrationDefs = {
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/sabnzbd.svg",
category: ["downloadClient", "usenet"],
documentationUrl: createDocumentationLink("/docs/integrations/sabnzbd"),
+ defaultPort: 8080,
+ apiKeySettingsPath: "/config/general/",
},
nzbGet: {
name: "NZBGet",
@@ -44,6 +48,7 @@ export const integrationDefs = {
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/nzbget.svg",
category: ["downloadClient", "usenet"],
documentationUrl: createDocumentationLink("/docs/integrations/nzbget"),
+ defaultPort: 6789,
},
deluge: {
name: "Deluge",
@@ -51,6 +56,7 @@ export const integrationDefs = {
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/deluge.svg",
category: ["downloadClient", "torrent"],
documentationUrl: createDocumentationLink("/docs/integrations/deluge"),
+ defaultPort: 8112,
},
transmission: {
name: "Transmission",
@@ -58,6 +64,7 @@ export const integrationDefs = {
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/transmission.svg",
category: ["downloadClient", "torrent"],
documentationUrl: createDocumentationLink("/docs/integrations/transmission"),
+ defaultPort: 9091,
},
qBittorrent: {
name: "qBittorrent",
@@ -65,6 +72,8 @@ export const integrationDefs = {
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/qbittorrent.svg",
category: ["downloadClient", "torrent"],
documentationUrl: createDocumentationLink("/docs/integrations/q-bittorent"),
+ defaultPort: 8080,
+ apiKeySettingsPath: "/#/settings/webui",
},
aria2: {
name: "Aria2",
@@ -72,6 +81,7 @@ export const integrationDefs = {
iconUrl: "https://cdn.jsdelivr.net/gh/PapirusDevelopmentTeam/papirus_icons@latest/src/system_downloads_3.svg",
category: ["downloadClient", "torrent", "miscellaneous"],
documentationUrl: createDocumentationLink("/docs/integrations/aria2"),
+ defaultPort: 6800,
},
slskd: {
name: "Slskd",
@@ -79,6 +89,7 @@ export const integrationDefs = {
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/slskd.svg",
category: ["downloadClient", "miscellaneous"],
documentationUrl: createDocumentationLink("/docs/integrations/slskd"),
+ defaultPort: 5030,
},
sonarr: {
name: "Sonarr",
@@ -86,6 +97,8 @@ export const integrationDefs = {
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/sonarr.svg",
category: ["calendar"],
documentationUrl: createDocumentationLink("/docs/integrations/sonarr"),
+ defaultPort: 8989,
+ apiKeySettingsPath: "/settings/general",
},
radarr: {
name: "Radarr",
@@ -93,6 +106,8 @@ export const integrationDefs = {
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/radarr.svg",
category: ["calendar"],
documentationUrl: createDocumentationLink("/docs/integrations/radarr"),
+ defaultPort: 7878,
+ apiKeySettingsPath: "/settings/general",
},
lidarr: {
name: "Lidarr",
@@ -100,6 +115,8 @@ export const integrationDefs = {
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/lidarr.svg",
category: ["calendar"],
documentationUrl: createDocumentationLink("/docs/integrations/lidarr"),
+ defaultPort: 8686,
+ apiKeySettingsPath: "/settings/general",
},
readarr: {
name: "Readarr",
@@ -107,6 +124,8 @@ export const integrationDefs = {
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/readarr.svg",
category: ["calendar"],
documentationUrl: createDocumentationLink("/docs/integrations/readarr"),
+ defaultPort: 8787,
+ apiKeySettingsPath: "/settings/general",
},
prowlarr: {
name: "Prowlarr",
@@ -114,6 +133,8 @@ export const integrationDefs = {
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/prowlarr.svg",
category: ["indexerManager"],
documentationUrl: createDocumentationLink("/docs/integrations/prowlarr"),
+ defaultPort: 9696,
+ apiKeySettingsPath: "/settings/general",
},
jellyfin: {
name: "Jellyfin",
@@ -121,6 +142,8 @@ export const integrationDefs = {
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/jellyfin.svg",
category: ["mediaService", "mediaRelease"],
documentationUrl: createDocumentationLink("/docs/integrations/jellyfin"),
+ defaultPort: 8096,
+ apiKeySettingsPath: "/web/index.html#!/dashboard/keys",
},
emby: {
name: "Emby",
@@ -128,6 +151,7 @@ export const integrationDefs = {
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/emby.svg",
category: ["mediaService", "mediaRelease"],
documentationUrl: createDocumentationLink("/docs/integrations/emby"),
+ defaultPort: 8096,
},
plex: {
name: "Plex",
@@ -135,6 +159,7 @@ export const integrationDefs = {
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/plex.svg",
category: ["mediaService", "mediaRelease"],
documentationUrl: createDocumentationLink("/docs/integrations/plex"),
+ defaultPort: 32400,
},
jellyseerr: {
name: "Jellyseerr",
@@ -142,6 +167,8 @@ export const integrationDefs = {
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/jellyseerr.svg",
category: ["mediaSearch", "mediaRequest", "search"],
documentationUrl: createDocumentationLink("/docs/integrations/jellyseerr"),
+ defaultPort: 5055,
+ apiKeySettingsPath: "/settings",
},
seerr: {
name: "Seerr",
@@ -149,6 +176,8 @@ export const integrationDefs = {
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/seerr.svg",
category: ["mediaSearch", "mediaRequest", "search"],
documentationUrl: createDocumentationLink("/docs/integrations/seerr"),
+ defaultPort: 5055,
+ apiKeySettingsPath: "/settings",
},
overseerr: {
name: "Overseerr",
@@ -156,6 +185,8 @@ export const integrationDefs = {
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/overseerr.svg",
category: ["mediaSearch", "mediaRequest", "search"],
documentationUrl: createDocumentationLink("/docs/integrations/overseerr"),
+ defaultPort: 5055,
+ apiKeySettingsPath: "/settings",
},
piHole: {
name: "Pi-hole",
@@ -163,6 +194,7 @@ export const integrationDefs = {
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/pi-hole.svg",
category: ["dnsHole"],
documentationUrl: createDocumentationLink("/docs/integrations/pi-hole"),
+ defaultPort: 80,
},
adGuardHome: {
name: "AdGuard Home",
@@ -170,6 +202,7 @@ export const integrationDefs = {
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/adguard-home.svg",
category: ["dnsHole"],
documentationUrl: createDocumentationLink("/docs/integrations/adguard-home"),
+ defaultPort: 3000,
},
homeAssistant: {
name: "Home Assistant",
@@ -177,6 +210,7 @@ export const integrationDefs = {
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/home-assistant.svg",
category: ["smartHomeServer", "calendar"],
documentationUrl: createDocumentationLink("/docs/integrations/home-assistant"),
+ defaultPort: 8123,
},
openmediavault: {
name: "OpenMediaVault",
@@ -184,6 +218,7 @@ export const integrationDefs = {
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/openmediavault.svg",
category: ["healthMonitoring"],
documentationUrl: createDocumentationLink("/docs/integrations/open-media-vault"),
+ defaultPort: 80,
},
dashDot: {
name: "Dash.",
@@ -198,6 +233,7 @@ export const integrationDefs = {
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/glances.svg",
category: ["healthMonitoring"],
documentationUrl: null,
+ defaultPort: 61208,
},
tdarr: {
name: "Tdarr",
@@ -205,6 +241,7 @@ export const integrationDefs = {
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/png/tdarr.png",
category: ["mediaTranscoding"],
documentationUrl: createDocumentationLink("/docs/integrations/tdarr"),
+ defaultPort: 8265,
},
proxmox: {
name: "Proxmox",
@@ -212,6 +249,7 @@ export const integrationDefs = {
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/proxmox.svg",
category: ["healthMonitoring"],
documentationUrl: createDocumentationLink("/docs/integrations/proxmox"),
+ defaultPort: 8006,
},
nextcloud: {
name: "Nextcloud",
@@ -219,6 +257,7 @@ export const integrationDefs = {
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/nextcloud.svg",
category: ["calendar", "notifications"],
documentationUrl: createDocumentationLink("/docs/integrations/nextcloud"),
+ defaultPort: 443,
},
unifiController: {
name: "Unifi Controller",
@@ -226,6 +265,7 @@ export const integrationDefs = {
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/png/unifi.png",
category: ["networkController"],
documentationUrl: createDocumentationLink("/docs/integrations/unifi-controller"),
+ defaultPort: 8443,
},
opnsense: {
name: "OPNsense",
@@ -233,6 +273,7 @@ export const integrationDefs = {
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/opnsense.svg",
category: ["firewall"],
documentationUrl: createDocumentationLink("/docs/integrations/opnsense"),
+ defaultPort: 443,
},
github: {
name: "Github",
@@ -304,6 +345,7 @@ export const integrationDefs = {
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/ntfy.svg",
category: ["notifications"],
documentationUrl: createDocumentationLink("/docs/integrations/ntfy"),
+ defaultPort: 80,
},
ical: {
name: "iCal",
@@ -318,6 +360,7 @@ export const integrationDefs = {
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/anchor.svg",
category: ["notes"],
documentationUrl: createDocumentationLink("/docs/integrations/anchor"),
+ defaultPort: 8080,
},
truenas: {
name: "TrueNAS",
@@ -325,6 +368,7 @@ export const integrationDefs = {
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/truenas.svg",
category: ["healthMonitoring"],
documentationUrl: createDocumentationLink("/docs/integrations/truenas"),
+ defaultPort: 80,
},
unraid: {
name: "Unraid",
@@ -332,20 +376,20 @@ export const integrationDefs = {
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/unraid.svg",
category: ["healthMonitoring"],
documentationUrl: createDocumentationLink("/docs/integrations/unraid"),
+ defaultPort: 80,
},
coolify: {
name: "Coolify",
secretKinds: [["apiKey"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/coolify.svg",
category: ["healthMonitoring"],
- // @ts-expect-error - docs page will be created when integration is merged
documentationUrl: createDocumentationLink("/docs/integrations/coolify"),
+ defaultPort: 8000,
},
searchCh: {
name: "Search.ch",
secretKinds: [[]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/search-ch.png",
- defaultUrl: "https://search.ch",
category: ["timetable"],
documentationUrl: createDocumentationLink("/docs/integrations/search-ch"),
},
@@ -355,6 +399,7 @@ export const integrationDefs = {
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/immich.svg",
category: ["photoService"],
documentationUrl: createDocumentationLink("/docs/integrations/immich"),
+ defaultPort: 2283,
},
tracearr: {
name: "Tracearr",
@@ -362,6 +407,7 @@ export const integrationDefs = {
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/tracearr.svg",
category: ["mediaMonitoring"],
documentationUrl: createDocumentationLink("/docs/integrations/tracearr"),
+ defaultPort: 7040,
},
speedtestTracker: {
name: "Speedtest Tracker",
@@ -369,6 +415,7 @@ export const integrationDefs = {
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/speedtest-tracker.png",
category: ["speedtest"],
documentationUrl: createDocumentationLink("/docs/integrations/speedtest-tracker"),
+ defaultPort: 80,
},
umami: {
name: "Umami",
@@ -377,6 +424,14 @@ export const integrationDefs = {
category: ["analytics"],
defaultUrl: "https://api.umami.is/v1",
documentationUrl: createDocumentationLink("/docs/integrations/umami"),
+ defaultPort: 3000,
+ },
+ peaNut: {
+ name: "PeaNUT",
+ secretKinds: [["username", "password"], []],
+ iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/peanut.svg",
+ category: ["ups"],
+ documentationUrl: null,
},
// This integration only returns mock data, it is used during development (but can also be used in production by directly going to the create page)
mock: {
@@ -396,6 +451,7 @@ export const integrationDefs = {
"networkController",
"notifications",
"smartHomeServer",
+ "ups",
],
documentationUrl: null,
},
@@ -418,6 +474,19 @@ export const getIntegrationDefaultUrl = (integration: IntegrationKind) => {
return "defaultUrl" in definition ? definition.defaultUrl : undefined;
};
+export const getIntegrationDefaultPort = (kind: IntegrationKind): number | undefined => {
+ const definition = integrationDefs[kind];
+ return "defaultPort" in definition ? definition.defaultPort : undefined;
+};
+
+export const getIntegrationApiKeyUrl = (integrationUrl: string, kind: IntegrationKind): string | null => {
+ const definition = integrationDefs[kind];
+ if (!("apiKeySettingsPath" in definition)) return null;
+ const base = removeTrailingSlash(integrationUrl);
+ if (!base) return null;
+ return `${base}${definition.apiKeySettingsPath}`;
+};
+
/**
* Get all integration kinds that share a category, typed only by the kinds belonging to the category
* @param category Category to filter by, belonging to IntegrationCategory
@@ -470,6 +539,7 @@ export const integrationCategories = [
"mediaMonitoring",
"speedtest",
"analytics",
+ "ups",
] as const;
export type IntegrationCategory = (typeof integrationCategories)[number];
diff --git a/packages/definitions/src/onboarding.ts b/packages/definitions/src/onboarding.ts
index d9a6ea03e..305a71f65 100644
--- a/packages/definitions/src/onboarding.ts
+++ b/packages/definitions/src/onboarding.ts
@@ -1,2 +1,2 @@
-export const onboardingSteps = ["start", "import", "user", "group", "settings", "finish"] as const;
+export const onboardingSteps = ["start", "import", "user", "group", "settings", "integrations", "finish"] as const;
export type OnboardingStep = (typeof onboardingSteps)[number];
diff --git a/packages/definitions/src/test/api-key-url.spec.ts b/packages/definitions/src/test/api-key-url.spec.ts
new file mode 100644
index 000000000..21b7e0fe7
--- /dev/null
+++ b/packages/definitions/src/test/api-key-url.spec.ts
@@ -0,0 +1,29 @@
+import { describe, expect, it } from "vitest";
+
+import { getIntegrationApiKeyUrl } from "../integration";
+
+describe("getIntegrationApiKeyUrl", () => {
+ it("builds URL for sonarr", () => {
+ expect(getIntegrationApiKeyUrl("http://192.168.1.10:8989", "sonarr")).toBe(
+ "http://192.168.1.10:8989/settings/general",
+ );
+ });
+
+ it("builds URL for jellyfin", () => {
+ expect(getIntegrationApiKeyUrl("http://media.local:8096", "jellyfin")).toBe(
+ "http://media.local:8096/web/index.html#!/dashboard/keys",
+ );
+ });
+
+ it("returns null for integrations without apiKeySettingsPath", () => {
+ expect(getIntegrationApiKeyUrl("http://localhost", "plex")).toBeNull();
+ });
+
+ it("returns null for empty URL", () => {
+ expect(getIntegrationApiKeyUrl("", "sonarr")).toBeNull();
+ });
+
+ it("strips trailing slash from base URL", () => {
+ expect(getIntegrationApiKeyUrl("http://sonarr.local/", "sonarr")).toBe("http://sonarr.local/settings/general");
+ });
+});
diff --git a/packages/definitions/src/test/docker-integration-match.spec.ts b/packages/definitions/src/test/docker-integration-match.spec.ts
new file mode 100644
index 000000000..2a5db3a14
--- /dev/null
+++ b/packages/definitions/src/test/docker-integration-match.spec.ts
@@ -0,0 +1,119 @@
+import { describe, expect, it } from "vitest";
+
+import {
+ extractContainerImageName,
+ matchIntegrationKind,
+ matchIntegrationKindFromContainer,
+} from "../docker-integration-match";
+
+describe("extractContainerImageName", () => {
+ it("extracts name from full image reference", () => {
+ expect(extractContainerImageName("linuxserver/sonarr:latest")).toBe("sonarr");
+ });
+
+ it("extracts name from ghcr image", () => {
+ expect(extractContainerImageName("ghcr.io/org/radarr:v5")).toBe("radarr");
+ });
+
+ it("handles plain image name", () => {
+ expect(extractContainerImageName("jellyfin")).toBe("jellyfin");
+ });
+
+ it("handles empty string", () => {
+ expect(extractContainerImageName("")).toBe("");
+ });
+});
+
+describe("matchIntegrationKind", () => {
+ it("matches exact kind name", () => {
+ expect(matchIntegrationKind("sonarr")).toBe("sonarr");
+ });
+
+ it("matches case-insensitively", () => {
+ expect(matchIntegrationKind("Sonarr")).toBe("sonarr");
+ });
+
+ it("matches icon slug for pi-hole", () => {
+ expect(matchIntegrationKind("pi-hole")).toBe("piHole");
+ });
+
+ it("matches alias pihole", () => {
+ expect(matchIntegrationKind("pihole")).toBe("piHole");
+ });
+
+ it("matches alias adguardhome", () => {
+ expect(matchIntegrationKind("adguardhome")).toBe("adGuardHome");
+ });
+
+ it("matches alias homeassistant", () => {
+ expect(matchIntegrationKind("homeassistant")).toBe("homeAssistant");
+ });
+
+ it("matches substring in image name", () => {
+ expect(matchIntegrationKind("hotio-sonarr")).toBe("sonarr");
+ });
+
+ it("returns null for unknown", () => {
+ expect(matchIntegrationKind("nginx")).toBeNull();
+ });
+
+ it("returns null for empty string", () => {
+ expect(matchIntegrationKind("")).toBeNull();
+ });
+});
+
+describe("matchIntegrationKindFromContainer", () => {
+ it("matches from image name first", () => {
+ expect(
+ matchIntegrationKindFromContainer({
+ image: "linuxserver/sonarr:latest",
+ name: "my-sonarr",
+ }),
+ ).toBe("sonarr");
+ });
+
+ it("falls back to container name", () => {
+ expect(
+ matchIntegrationKindFromContainer({
+ image: "custom/myapp:latest",
+ name: "radarr",
+ }),
+ ).toBe("radarr");
+ });
+
+ it("returns null when neither matches", () => {
+ expect(
+ matchIntegrationKindFromContainer({
+ image: "nginx:latest",
+ name: "web-proxy",
+ }),
+ ).toBeNull();
+ });
+
+ it("excludes dockerHub even when image contains docker", () => {
+ expect(
+ matchIntegrationKindFromContainer({
+ image: "portainer/portainer-ce:latest",
+ name: "docker-proxy",
+ }),
+ ).toBeNull();
+ });
+
+ it("excludes ical from container matching", () => {
+ expect(
+ matchIntegrationKindFromContainer({
+ image: "ical-relay:latest",
+ name: "ical",
+ }),
+ ).toBeNull();
+ });
+
+ it("excludes github from container matching", () => {
+ expect(
+ matchIntegrationKindFromContainer({
+ image: "github-runner:latest",
+ name: "github-actions",
+ }),
+ ).toBeNull();
+ });
+});
diff --git a/packages/definitions/src/test/integration-url-template.spec.ts b/packages/definitions/src/test/integration-url-template.spec.ts
new file mode 100644
index 000000000..05e7a4e9b
--- /dev/null
+++ b/packages/definitions/src/test/integration-url-template.spec.ts
@@ -0,0 +1,41 @@
+import { describe, expect, it } from "vitest";
+
+import { getIntegrationDefaultPort } from "../integration";
+import { buildIntegrationUrl } from "../integration-url-template";
+
+describe("buildIntegrationUrl", () => {
+ it("builds subdomain URL", () => {
+ expect(buildIntegrationUrl("sonarr", "homelab.local", "subdomain")).toBe("https://sonarr.homelab.local");
+ });
+
+ it("builds hostPort URL with default port", () => {
+ expect(buildIntegrationUrl("sonarr", "192.168.1.1", "hostPort")).toBe("http://192.168.1.1:8989");
+ });
+
+ it("uses docker port over default port", () => {
+ expect(buildIntegrationUrl("sonarr", "192.168.1.1", "hostPort", 9999)).toBe("http://192.168.1.1:9999");
+ });
+
+ it("builds hostPort URL without port when none known", () => {
+ expect(buildIntegrationUrl("mock", "192.168.1.1", "hostPort")).toBe("http://192.168.1.1");
+ });
+
+ it("strips trailing slashes from host", () => {
+ expect(buildIntegrationUrl("sonarr", "homelab.local/", "subdomain")).toBe("https://sonarr.homelab.local");
+ expect(buildIntegrationUrl("sonarr", "homelab.local///", "subdomain")).toBe("https://sonarr.homelab.local");
+ });
+
+ it("returns empty string for empty host", () => {
+ expect(buildIntegrationUrl("sonarr", "", "subdomain")).toBe("");
+ });
+
+ it("uses pi-hole slug for subdomain mode", () => {
+ expect(buildIntegrationUrl("piHole", "example.com", "subdomain")).toBe("https://pi-hole.example.com");
+ });
+
+ it("reads default ports from integration definitions", () => {
+ expect(getIntegrationDefaultPort("radarr")).toBe(7878);
+ expect(getIntegrationDefaultPort("prowlarr")).toBe(9696);
+ expect(getIntegrationDefaultPort("jellyfin")).toBe(8096);
+ });
+});
diff --git a/packages/definitions/src/widget-integration-map.ts b/packages/definitions/src/widget-integration-map.ts
new file mode 100644
index 000000000..bef83fea6
--- /dev/null
+++ b/packages/definitions/src/widget-integration-map.ts
@@ -0,0 +1,113 @@
+import { createDocumentationLink } from "./docs";
+import type { IntegrationKind } from "./integration";
+import { getIntegrationKindsByCategory } from "./integration";
+import type { WidgetKind } from "./widget";
+
+/**
+ * Static mapping of widget kinds to the integration kinds they support.
+ * Mirrors the supportedIntegrations declared in each widget definition
+ * but lives in @homarr/definitions so server-side code (API layer)
+ * can use it without importing @homarr/widgets.
+ */
+export const widgetIntegrationSupport: Partial> = {
+ dnsHoleSummary: getIntegrationKindsByCategory("dnsHole"),
+ dnsHoleControls: getIntegrationKindsByCategory("dnsHole"),
+ "smartHome-entityState": getIntegrationKindsByCategory("smartHomeServer"),
+ "smartHome-executeAutomation": getIntegrationKindsByCategory("smartHomeServer"),
+ mediaServer: getIntegrationKindsByCategory("mediaService"),
+ calendar: getIntegrationKindsByCategory("calendar"),
+ downloads: getIntegrationKindsByCategory("downloadClient"),
+ "mediaRequests-requestList": getIntegrationKindsByCategory("mediaRequest"),
+ "mediaRequests-requestStats": getIntegrationKindsByCategory("mediaRequest"),
+ mediaTranscoding: getIntegrationKindsByCategory("mediaTranscoding"),
+ networkControllerSummary: getIntegrationKindsByCategory("networkController"),
+ networkControllerStatus: getIntegrationKindsByCategory("networkController"),
+ indexerManager: getIntegrationKindsByCategory("indexerManager"),
+ healthMonitoring: getIntegrationKindsByCategory("healthMonitoring"),
+ firewall: getIntegrationKindsByCategory("firewall"),
+ notifications: getIntegrationKindsByCategory("notifications"),
+ mediaReleases: ["emby", "jellyfin", "plex"],
+ systemResources: ["dashDot", "openmediavault", "truenas", "unraid", "glances"],
+ systemDisks: ["dashDot", "openmediavault", "truenas", "unraid"],
+ coolify: ["coolify"],
+ timetable: ["searchCh"],
+ "immich-serverStats": ["immich"],
+ "immich-albumCarousel": ["immich"],
+ tracearr: ["tracearr"],
+ speedtestTracker: ["speedtestTracker"],
+ anchorNote: ["anchor"],
+};
+
+export const getWidgetKindsForIntegration = (integrationKind: IntegrationKind): WidgetKind[] => {
+ const result: WidgetKind[] = [];
+ for (const [widgetKind, supportedIntegrations] of Object.entries(widgetIntegrationSupport)) {
+ if (supportedIntegrations.includes(integrationKind)) {
+ result.push(widgetKind as WidgetKind);
+ }
+ }
+ return result;
+};
+
+export interface DefaultWidgetConfig {
+ kind: WidgetKind;
+ width: number;
+ height: number;
+ options?: Record;
+ skip?: boolean;
+}
+
+export const defaultWidgetConfigs: DefaultWidgetConfig[] = [
+ { kind: "clock", width: 2, height: 1 },
+ { kind: "weather", width: 2, height: 1, options: { showCity: true, hasForecast: true, forecastDayCount: 3 } },
+ { kind: "bookmarks", width: 2, height: 2, options: { title: "Useful Links", layout: "grid", openNewTab: true } },
+];
+
+export const generalWidgets: WidgetKind[] = defaultWidgetConfigs
+ .filter((config) => !config.skip)
+ .map((config) => config.kind);
+
+export const featuredIntegrations: readonly IntegrationKind[] = [
+ "sonarr",
+ "radarr",
+ "prowlarr",
+ "sabNzbd",
+ "qBittorrent",
+ "seerr",
+ "jellyfin",
+];
+
+export const hiddenFromOnboarding = new Set([
+ "searchCh",
+ "github",
+ "dockerHub",
+ "gitlab",
+ "npm",
+ "codeberg",
+ "linuxServerIO",
+ "gitHubContainerRegistry",
+ "quay",
+ "mock",
+]);
+
+export const defaultBookmarkApps = [
+ {
+ name: "Homarr Docs",
+ href: createDocumentationLink("/docs/getting-started"),
+ iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/homarr.svg",
+ },
+ {
+ name: "Homarr GitHub",
+ href: "https://github.com/homarr-labs/homarr",
+ iconUrl: "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/svg/github.svg",
+ },
+ {
+ name: "Help Translate",
+ href: createDocumentationLink("/docs/community/translations"),
+ iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/homarr.svg",
+ },
+ {
+ name: "Support Homarr",
+ href: "https://opencollective.com/homarr",
+ iconUrl: "https://cdn.jsdelivr.net/gh/loganmarchione/homelab-svg-assets@latest/assets/opencollective.svg",
+ },
+];
diff --git a/packages/definitions/src/widget.ts b/packages/definitions/src/widget.ts
index d6305a08e..fec9eb719 100644
--- a/packages/definitions/src/widget.ts
+++ b/packages/definitions/src/widget.ts
@@ -38,5 +38,6 @@ export const widgetKinds = [
"tracearr",
"speedtestTracker",
"umami",
+ "ups",
] as const;
export type WidgetKind = (typeof widgetKinds)[number];
diff --git a/packages/docker/src/shared.ts b/packages/docker/src/shared.ts
index f50bd32c7..e98695278 100644
--- a/packages/docker/src/shared.ts
+++ b/packages/docker/src/shared.ts
@@ -11,3 +11,23 @@ export const containerStateColorMap = {
removing: "pink",
dead: "dark",
} satisfies Record;
+
+export const memoryUsageColor = (number: number, state: string): MantineColor => {
+ const mbUsage = number / 1024 / 1024;
+ if (mbUsage === 0 && state !== "running") return "red";
+ if (mbUsage < 128) return "green";
+ if (mbUsage < 256) return "yellow";
+ if (mbUsage < 512) return "orange";
+ return "red";
+};
+
+export const cpuUsageColor = (number: number, state: string): MantineColor => {
+ if (number === 0 && state !== "running") return "red";
+ if (number < 40) return "green";
+ if (number < 60) return "yellow";
+ if (number < 90) return "orange";
+ return "red";
+};
+
+export const safeValue = (value?: number, fallback = 0): number =>
+ value !== undefined && !isNaN(value) ? value : fallback;
diff --git a/packages/forms-collection/src/index.tsx b/packages/forms-collection/src/index.tsx
index 218655cb5..7621bed43 100644
--- a/packages/forms-collection/src/index.tsx
+++ b/packages/forms-collection/src/index.tsx
@@ -5,3 +5,5 @@ export * from "./icon-picker/icon-picker";
export * from "./new-app/icon-matcher";
export * from "./upload-media/upload-media";
+
+export * from "./user-create/user-create-password-fields";
diff --git a/packages/forms-collection/src/new-app/_form.tsx b/packages/forms-collection/src/new-app/_form.tsx
index 05c7e3f6f..2bce21034 100644
--- a/packages/forms-collection/src/new-app/_form.tsx
+++ b/packages/forms-collection/src/new-app/_form.tsx
@@ -1,7 +1,7 @@
"use client";
import type { ChangeEventHandler } from "react";
-import { useEffect, useRef } from "react";
+import { useEffect, useImperativeHandle, useRef } from "react";
import { Button, Checkbox, Collapse, Group, Stack, Textarea, TextInput } from "@mantine/core";
import { useDebouncedValue, useDisclosure } from "@mantine/hooks";
import type { z } from "zod/v4";
@@ -17,6 +17,19 @@ import { findBestIconMatch } from "./icon-matcher";
type FormType = z.infer;
+const toFormValues = (values: FormType | undefined): FormType => ({
+ name: values?.name ?? "",
+ description: values?.description ?? "",
+ iconUrl: values?.iconUrl ?? "",
+ href: values?.href ?? "",
+ pingUrl: values?.pingUrl ?? "",
+});
+
+export interface AppFormHandle {
+ submit: () => Promise;
+ isDirty: () => boolean;
+}
+
interface AppFormProps {
showBackToOverview: boolean;
buttonLabels: {
@@ -24,8 +37,10 @@ interface AppFormProps {
submitAndCreateAnother?: string;
};
initialValues?: FormType;
- handleSubmit: (values: FormType, redirect: boolean, afterSuccess?: () => void) => void;
+ handleSubmit: (values: FormType, redirect: boolean, afterSuccess?: () => void) => void | Promise;
isPending: boolean;
+ hideButtons?: boolean;
+ formRef?: React.Ref;
}
export const AppForm = ({
@@ -34,19 +49,31 @@ export const AppForm = ({
handleSubmit: originalHandleSubmit,
initialValues,
isPending,
+ hideButtons,
+ formRef,
}: AppFormProps) => {
const t = useI18n();
const form = useZodForm(appManageSchema, {
- initialValues: {
- name: initialValues?.name ?? "",
- description: initialValues?.description ?? "",
- iconUrl: initialValues?.iconUrl ?? "",
- href: initialValues?.href ?? "",
- pingUrl: initialValues?.pingUrl ?? "",
- },
+ initialValues: toFormValues(initialValues),
});
+ const initialValuesKey = [
+ initialValues?.name,
+ initialValues?.description,
+ initialValues?.iconUrl,
+ initialValues?.href,
+ initialValues?.pingUrl,
+ ].join("\0");
+
+ useEffect(() => {
+ if (!initialValues || form.isDirty()) {
+ return;
+ }
+
+ form.initialize(toFormValues(initialValues));
+ }, [initialValuesKey, form, initialValues]);
+
// Debounce the name value with 200ms delay
const [debouncedName] = useDebouncedValue(form.values.name, 200);
@@ -62,6 +89,29 @@ export const AppForm = ({
originalHandleSubmit(values, redirect, afterSuccess);
};
+ useImperativeHandle(
+ formRef,
+ () => ({
+ submit: () =>
+ new Promise((resolve) => {
+ form.onSubmit(
+ async (values) => {
+ try {
+ await Promise.resolve(handleSubmit(values));
+ form.initialize(values);
+ resolve(true);
+ } catch {
+ resolve(false);
+ }
+ },
+ () => resolve(false),
+ )();
+ }),
+ isDirty: () => form.isDirty(),
+ }),
+ [form, handleSubmit],
+ );
+
const [opened, { open, close }] = useDisclosure((initialValues?.pingUrl?.length ?? 0) > 0);
const handleClickDifferentUrlPing: ChangeEventHandler = () => {
@@ -92,32 +142,32 @@ export const AppForm = ({
}
}, [debouncedName, iconsData]);
- return (
-
+ )}
+
);
+
+ if (hideButtons) {
+ return formFields;
+ }
+
+ return ;
};
diff --git a/packages/forms-collection/src/user-create/user-create-password-fields.tsx b/packages/forms-collection/src/user-create/user-create-password-fields.tsx
new file mode 100644
index 000000000..ad7329307
--- /dev/null
+++ b/packages/forms-collection/src/user-create/user-create-password-fields.tsx
@@ -0,0 +1,33 @@
+"use client";
+
+import { PasswordInput, Stack } from "@mantine/core";
+import type { PasswordInputProps } from "@mantine/core";
+
+import { useScopedI18n } from "@homarr/translation/client";
+import { CustomPasswordInput } from "@homarr/ui";
+
+interface UserCreatePasswordFieldsProps {
+ passwordInputProps: PasswordInputProps;
+ confirmPasswordInputProps: PasswordInputProps;
+ variant?: PasswordInputProps["variant"];
+}
+
+export const UserCreatePasswordFields = ({
+ passwordInputProps,
+ confirmPasswordInputProps,
+ variant,
+}: UserCreatePasswordFieldsProps) => {
+ const t = useScopedI18n("user.field");
+
+ return (
+
+
+
+
+ );
+};
diff --git a/packages/integrations/src/base/creator.ts b/packages/integrations/src/base/creator.ts
index 9e2466bea..20688b708 100644
--- a/packages/integrations/src/base/creator.ts
+++ b/packages/integrations/src/base/creator.ts
@@ -36,6 +36,7 @@ import { NTFYIntegration } from "../ntfy/ntfy-integration";
import { OpenMediaVaultIntegration } from "../openmediavault/openmediavault-integration";
import { OPNsenseIntegration } from "../opnsense/opnsense-integration";
import { OverseerrIntegration } from "../overseerr/overseerr-integration";
+import { PeaNutIntegration } from "../peanut/peanut-integration";
import { createPiHoleIntegrationAsync } from "../pi-hole/pi-hole-integration-factory";
import { PlexIntegration } from "../plex/plex-integration";
import { ProwlarrIntegration } from "../prowlarr/prowlarr-integration";
@@ -123,6 +124,7 @@ export const integrationCreators = {
immich: ImmichIntegration,
speedtestTracker: SpeedtestTrackerIntegration,
umami: UmamiIntegration,
+ peaNut: PeaNutIntegration,
} satisfies Record Promise]>;
type IntegrationInstanceOfKind = {
diff --git a/packages/integrations/src/index.ts b/packages/integrations/src/index.ts
index c7451265c..198a17671 100644
--- a/packages/integrations/src/index.ts
+++ b/packages/integrations/src/index.ts
@@ -34,6 +34,7 @@ export { ImmichIntegration } from "./immich/immich-integration";
export { TracearrIntegration } from "./tracearr/tracearr-integration";
export { SpeedtestTrackerIntegration } from "./speedtest-tracker/speedtest-tracker-integration";
export { UmamiIntegration } from "./umami/umami-integration";
+export { PeaNutIntegration } from "./peanut/peanut-integration";
// Types
export type { IntegrationInput } from "./base/integration";
diff --git a/packages/integrations/src/interfaces/media-requests/media-request-types.ts b/packages/integrations/src/interfaces/media-requests/media-request-types.ts
index 4d7b094ff..d475764bf 100644
--- a/packages/integrations/src/interfaces/media-requests/media-request-types.ts
+++ b/packages/integrations/src/interfaces/media-requests/media-request-types.ts
@@ -12,12 +12,14 @@ interface SeriesInformation {
overview: string;
seasons: SerieSeason[];
posterPath: string;
+ requestedSeasons?: number[];
}
interface MovieInformation {
id: number;
overview: string;
posterPath: string;
+ requestedSeasons?: number[];
}
export type MediaInformation = SeriesInformation | MovieInformation;
diff --git a/packages/integrations/src/interfaces/ups-summary/ups-summary-integration.ts b/packages/integrations/src/interfaces/ups-summary/ups-summary-integration.ts
new file mode 100644
index 000000000..21510954d
--- /dev/null
+++ b/packages/integrations/src/interfaces/ups-summary/ups-summary-integration.ts
@@ -0,0 +1,5 @@
+import type { UpsSummary } from "./ups-summary-types";
+
+export interface IUpsSummaryIntegration {
+ getUpsSummariesAsync(): Promise;
+}
diff --git a/packages/integrations/src/interfaces/ups-summary/ups-summary-types.ts b/packages/integrations/src/interfaces/ups-summary/ups-summary-types.ts
new file mode 100644
index 000000000..6979de023
--- /dev/null
+++ b/packages/integrations/src/interfaces/ups-summary/ups-summary-types.ts
@@ -0,0 +1,32 @@
+/**
+ * Normalized UPS status, resolved from the raw NUT status flags by the integration so that
+ * widgets and future UPS integrations share a single, typed vocabulary.
+ */
+export const upsStatuses = ["online", "charging", "onBattery", "lowBattery", "unknown"] as const;
+
+export type UpsStatus = (typeof upsStatuses)[number];
+
+export interface UpsSummary {
+ /** Stable identifier of the UPS device on the NUT server */
+ id: string;
+ /** Human readable name, derived from the manufacturer and model */
+ name: string;
+ manufacturer: string | null;
+ model: string | null;
+ serial: string | null;
+ /** Normalized status, resolved from the raw NUT `ups.status` flags */
+ status: UpsStatus;
+ /** Battery charge in percent */
+ batteryCharge: number | null;
+ /** Estimated remaining runtime on battery, in seconds */
+ batteryRuntime: number | null;
+ batteryVoltage: number | null;
+ /** UPS load in percent */
+ load: number | null;
+ inputVoltage: number | null;
+ outputVoltage: number | null;
+ /** Real power drawn by the load, in watts */
+ power: number | null;
+ /** UPS temperature in degrees celsius */
+ temperature: number | null;
+}
diff --git a/packages/integrations/src/mock/data/calendar.ts b/packages/integrations/src/mock/data/calendar.ts
index 530819dd3..4ebd36d6e 100644
--- a/packages/integrations/src/mock/data/calendar.ts
+++ b/packages/integrations/src/mock/data/calendar.ts
@@ -1,24 +1,130 @@
import type { ICalendarIntegration } from "../../interfaces/calendar/calendar-integration";
import type { CalendarEvent } from "../../interfaces/calendar/calendar-types";
-export class CalendarMockService implements ICalendarIntegration {
- public async getCalendarEventsAsync(start: Date, end: Date, _includeUnmonitored: boolean): Promise {
- const result = [homarrMeetup(start, end), titanicRelease(start, end), seriesRelease(start, end)];
- return await Promise.resolve(result);
- }
+interface EventTemplate {
+ title: string;
+ subTitle: string;
+ description: string;
+ dayOffset: number;
+ hours: number;
+ image: CalendarEvent["image"];
+ indicatorColor: string;
+ metadata?: CalendarEvent["metadata"];
+ location?: string;
+ links: CalendarEvent["links"];
}
-const homarrMeetup = (start: Date, end: Date): CalendarEvent => {
- const startDate = randomDateBetween(start, end);
- const endDate = new Date(startDate.getTime() + 2 * 60 * 60 * 1000); // 2 hours later
- return {
+const eventTemplates: EventTemplate[] = [
+ {
+ title: "The Mandalorian",
+ subTitle: "Season 4",
+ description: "A lone bounty hunter in the outer reaches of the galaxy.",
+ dayOffset: 0,
+ hours: 20,
+ image: {
+ src: "https://image.tmdb.org/t/p/w300/sWgBv7LV2PRoQgkxwlibdGXKz1S.jpg",
+ aspectRatio: { width: 7, height: 12 },
+ badge: { content: "S4/E3", color: "red" },
+ },
+ indicatorColor: "blue",
+ links: [
+ {
+ href: "https://www.imdb.com/title/tt8111088/",
+ name: "IMDb",
+ color: "#f5c518",
+ isDark: false,
+ logo: "/images/apps/imdb.svg",
+ },
+ ],
+ },
+ {
+ title: "Silo",
+ subTitle: "Season 2",
+ description: "In a ruined and toxic future, thousands live in a giant underground silo.",
+ dayOffset: 3,
+ hours: 21,
+ image: {
+ src: "https://image.tmdb.org/t/p/w300/2xSzAuKMhERTcPA19SpFQtVrMC0.jpg",
+ aspectRatio: { width: 7, height: 12 },
+ badge: { content: "S2/E8", color: "teal" },
+ },
+ indicatorColor: "teal",
+ links: [
+ {
+ href: "https://www.imdb.com/title/tt14688458/",
+ name: "IMDb",
+ color: "#f5c518",
+ isDark: false,
+ logo: "/images/apps/imdb.svg",
+ },
+ ],
+ },
+ {
+ title: "Dune: Part Two",
+ subTitle: "Digital Release",
+ description: "Paul Atreides unites with the Fremen to seek revenge against the conspirators.",
+ dayOffset: 5,
+ hours: 0,
+ image: {
+ src: "https://image.tmdb.org/t/p/w300/8b8R8l88Qje9dn9OE8PY05Nez7.jpg",
+ aspectRatio: { width: 7, height: 12 },
+ },
+ metadata: { type: "radarr", releaseType: "digitalRelease" },
+ indicatorColor: "cyan",
+ links: [
+ {
+ href: "https://www.imdb.com/title/tt15239678/",
+ name: "IMDb",
+ color: "#f5c518",
+ isDark: false,
+ logo: "/images/apps/imdb.svg",
+ },
+ ],
+ },
+ {
+ title: "Severance",
+ subTitle: "Season 2",
+ description: "Mark leads a team of office workers whose memories are surgically divided.",
+ dayOffset: 7,
+ hours: 22,
+ image: {
+ src: "https://image.tmdb.org/t/p/w300/pJAtR4gMFnalHCmT9cByPFgkZ6B.jpg",
+ aspectRatio: { width: 7, height: 12 },
+ badge: { content: "S2/E5", color: "violet" },
+ },
+ indicatorColor: "violet",
+ links: [],
+ },
+ {
+ title: "Oppenheimer",
+ subTitle: "Physical Release",
+ description: "The story of the development of the atomic bomb.",
+ dayOffset: 10,
+ hours: 0,
+ image: {
+ src: "https://image.tmdb.org/t/p/w300/8Gxv8gSFCU0XGDykEGv7zR1n2ua.jpg",
+ aspectRatio: { width: 7, height: 12 },
+ },
+ metadata: { type: "radarr", releaseType: "physicalRelease" },
+ indicatorColor: "orange",
+ links: [
+ {
+ href: "https://www.imdb.com/title/tt15398776/",
+ name: "IMDb",
+ color: "#f5c518",
+ isDark: false,
+ logo: "/images/apps/imdb.svg",
+ },
+ ],
+ },
+ {
title: "Homarr Meetup",
subTitle: "",
description: "Yearly meetup of the Homarr community",
- startDate,
- endDate,
- image: null,
+ dayOffset: 14,
+ hours: 14,
location: "Mountains",
+ image: null,
indicatorColor: "#fa5252",
links: [
{
@@ -29,63 +135,79 @@ const homarrMeetup = (start: Date, end: Date): CalendarEvent => {
isDark: true,
},
],
- };
+ },
+ {
+ title: "The Bear",
+ subTitle: "Season 3",
+ description: "A young chef fights to transform a sandwich shop.",
+ dayOffset: 18,
+ hours: 3,
+ image: {
+ src: "https://image.tmdb.org/t/p/w300/sHFlYJhNbmpMOkr7JKGxFAnMPFm.jpg",
+ aspectRatio: { width: 7, height: 12 },
+ badge: { content: "S3/E1", color: "yellow" },
+ },
+ indicatorColor: "yellow",
+ links: [],
+ },
+ {
+ title: "Foundation",
+ subTitle: "Season 3",
+ description: "A band of exiles discovers that the only way to save the Galactic Empire is to defy it.",
+ dayOffset: 21,
+ hours: 20,
+ image: {
+ src: "https://image.tmdb.org/t/p/w300/wSByZTwCPVYAjlqcVCBTCLZchkC.jpg",
+ aspectRatio: { width: 7, height: 12 },
+ badge: { content: "S3/E1", color: "indigo" },
+ },
+ indicatorColor: "indigo",
+ links: [],
+ },
+ {
+ title: "Blade Runner 2099",
+ subTitle: "Premiere",
+ description: "A new chapter in the Blade Runner universe.",
+ dayOffset: 25,
+ hours: 21,
+ image: {
+ src: "https://image.tmdb.org/t/p/w300/63UMsIFOlCSz3uCPKtbgOdUr4pE.jpg",
+ aspectRatio: { width: 7, height: 12 },
+ },
+ metadata: { type: "radarr", releaseType: "digitalRelease" },
+ indicatorColor: "grape",
+ links: [],
+ },
+];
+
+const buildEventsForRange = (start: Date, end: Date): CalendarEvent[] => {
+ const rangeStart = new Date(start);
+ rangeStart.setDate(rangeStart.getDate() - 3);
+
+ return eventTemplates
+ .map((tpl) => {
+ const startDate = new Date(rangeStart);
+ startDate.setDate(startDate.getDate() + tpl.dayOffset);
+ startDate.setHours(tpl.hours, 0, 0, 0);
+
+ return {
+ title: tpl.title,
+ subTitle: tpl.subTitle,
+ description: tpl.description,
+ startDate,
+ endDate: tpl.location ? new Date(startDate.getTime() + 2 * 60 * 60 * 1000) : null,
+ image: tpl.image,
+ location: tpl.location ?? null,
+ indicatorColor: tpl.indicatorColor,
+ links: tpl.links,
+ ...(tpl.metadata ? { metadata: tpl.metadata } : {}),
+ };
+ })
+ .filter((event) => event.startDate >= start && event.startDate <= end);
};
-const titanicRelease = (start: Date, end: Date): CalendarEvent => ({
- title: "Titanic",
- subTitle: "A classic movie",
- description: "A tragic love story set on the ill-fated RMS Titanic.",
- startDate: randomDateBetween(start, end),
- endDate: null,
- image: {
- src: "https://image.tmdb.org/t/p/original/5bTWA20cL9LCIGNpde4Epc2Ijzn.jpg",
- aspectRatio: { width: 7, height: 12 },
- },
- location: null,
- metadata: {
- type: "radarr",
- releaseType: "inCinemas",
- },
- indicatorColor: "cyan",
- links: [
- {
- href: "https://www.imdb.com/title/tt0120338/",
- name: "IMDb",
- color: "#f5c518",
- isDark: false,
- logo: "/images/apps/imdb.svg",
- },
- ],
-});
-
-const seriesRelease = (start: Date, end: Date): CalendarEvent => ({
- title: "The Mandalorian",
- subTitle: "A Star Wars Series",
- description: "A lone bounty hunter in the outer reaches of the galaxy.",
- startDate: randomDateBetween(start, end),
- endDate: null,
- image: {
- src: "https://image.tmdb.org/t/p/original/sWgBv7LV2PRoQgkxwlibdGXKz1S.jpg",
- aspectRatio: { width: 7, height: 12 },
- badge: {
- content: "S1/E1",
- color: "red",
- },
- },
- location: null,
- indicatorColor: "blue",
- links: [
- {
- href: "https://www.imdb.com/title/tt8111088/",
- name: "IMDb",
- color: "#f5c518",
- isDark: false,
- logo: "/images/apps/imdb.svg",
- },
- ],
-});
-
-function randomDateBetween(start: Date, end: Date): Date {
- return new Date(start.getTime() + Math.random() * (end.getTime() - start.getTime()));
+export class CalendarMockService implements ICalendarIntegration {
+ public async getCalendarEventsAsync(start: Date, end: Date, _includeUnmonitored: boolean): Promise {
+ return await Promise.resolve(buildEventsForRange(start, end));
+ }
}
diff --git a/packages/integrations/src/mock/data/download.ts b/packages/integrations/src/mock/data/download.ts
index 7d3ab0252..391315fe4 100644
--- a/packages/integrations/src/mock/data/download.ts
+++ b/packages/integrations/src/mock/data/download.ts
@@ -2,57 +2,163 @@ import type { DownloadClientJobsAndStatus } from "../../interfaces/downloads/dow
import type { IDownloadClientIntegration } from "../../interfaces/downloads/download-client-integration";
import type { DownloadClientItem } from "../../interfaces/downloads/download-client-items";
+const mockItems: Omit[] = [
+ {
+ name: "Big.Buck.Bunny.2008.1080p.BluRay.x264",
+ type: "torrent",
+ size: 4_700_000_000,
+ progress: 0.47,
+ state: "leeching",
+ downSpeed: 12_400_000,
+ upSpeed: 890_000,
+ sent: 420_000_000,
+ received: 2_209_000_000,
+ time: 201_000,
+ category: "movies",
+ },
+ {
+ name: "Ubuntu.24.04.1.LTS.Desktop.amd64.iso",
+ type: "torrent",
+ size: 5_800_000_000,
+ progress: 0.82,
+ state: "leeching",
+ downSpeed: 34_500_000,
+ upSpeed: 2_100_000,
+ sent: 1_200_000_000,
+ received: 4_756_000_000,
+ time: 30_000,
+ category: "iso",
+ },
+ {
+ name: "Sintel.2010.4K.DTS-HD.MA.5.1.mkv",
+ type: "torrent",
+ size: 18_200_000_000,
+ progress: 0.03,
+ state: "leeching",
+ downSpeed: 1_200_000,
+ upSpeed: 50_000,
+ sent: 30_000_000,
+ received: 546_000_000,
+ time: 14_880_000,
+ category: "movies",
+ },
+ {
+ name: "Arch.Linux.2024.12.01.x86_64.iso",
+ type: "torrent",
+ size: 1_100_000_000,
+ progress: 1,
+ state: "seeding",
+ downSpeed: 0,
+ upSpeed: 4_500_000,
+ sent: 3_300_000_000,
+ received: 1_100_000_000,
+ time: -7_200_000,
+ category: "iso",
+ },
+ {
+ name: "Tears.of.Steel.2012.1080p.DTS.mkv",
+ type: "torrent",
+ size: 7_340_000_000,
+ progress: 0,
+ state: "paused",
+ downSpeed: 0,
+ upSpeed: 0,
+ time: 0,
+ category: "movies",
+ },
+ {
+ name: "Cosmos.Laundromat.First.Cycle.2015.4K.mkv",
+ type: "torrent",
+ size: 12_000_000_000,
+ progress: 0.65,
+ state: "stalled",
+ downSpeed: 0,
+ upSpeed: 0,
+ sent: 200_000_000,
+ received: 7_800_000_000,
+ time: 0,
+ category: "movies",
+ },
+ {
+ name: "Spring.2019.4K.HDR.mkv",
+ type: "usenet",
+ size: 3_200_000_000,
+ progress: 0.91,
+ state: "downloading",
+ downSpeed: 45_000_000,
+ time: 6_500,
+ category: "movies",
+ },
+ {
+ name: "Agent.327.Operation.Barbershop.2017.1080p.mkv",
+ type: "usenet",
+ size: 1_800_000_000,
+ progress: 1,
+ state: "completed",
+ time: -3_600_000,
+ category: "movies",
+ },
+ {
+ name: "Caminandes.3.Llamigos.2016.1080p.mkv",
+ type: "torrent",
+ size: 890_000_000,
+ progress: 1,
+ state: "seeding",
+ downSpeed: 0,
+ upSpeed: 1_800_000,
+ sent: 2_670_000_000,
+ received: 890_000_000,
+ time: -86_400_000,
+ category: "movies",
+ },
+ {
+ name: "Hero.2024.S01.1080p.WEB-DL.DDP5.1",
+ type: "usenet",
+ size: 9_400_000_000,
+ progress: 0.22,
+ state: "downloading",
+ downSpeed: 28_000_000,
+ time: 244_000,
+ category: "tv",
+ },
+];
+
export class DownloadClientMockService implements IDownloadClientIntegration {
public async getClientJobsAndStatusAsync(input: { limit: number }): Promise {
return await Promise.resolve({
status: {
- paused: Math.random() < 0.5,
+ paused: false,
rates: {
- down: Math.floor(Math.random() * 5000),
- up: Math.floor(Math.random() * 5000),
+ down: 121_100_000,
+ up: 9_340_000,
},
- types: ["torrent", "usenet"],
+ types: ["torrent", "usenet"] as const,
},
- items: Array.from({ length: 20 }, (_, index) => DownloadClientMockService.createItem(index)).slice(
- 0,
- input.limit,
- ),
+ items: mockItems.slice(0, input.limit).map((item, index) => ({
+ ...item,
+ id: `mock-${index}`,
+ index,
+ })),
});
}
public async pauseQueueAsync(): Promise {
- return await Promise.resolve();
+ await Promise.resolve();
}
public async pauseItemAsync(_item: DownloadClientItem): Promise {
- return await Promise.resolve();
+ await Promise.resolve();
}
public async resumeQueueAsync(): Promise {
- return Promise.resolve();
+ await Promise.resolve();
}
public async resumeItemAsync(_item: DownloadClientItem): Promise {
- return await Promise.resolve();
+ await Promise.resolve();
}
public async deleteItemAsync(_item: DownloadClientItem, _fromDisk: boolean): Promise {
- return await Promise.resolve();
- }
-
- private static createItem(index: number): DownloadClientItem {
- const progress = Math.random() < 0.5 ? Math.random() : 1;
- return {
- id: `item-${index}`,
- index,
- name: `Item ${index}`,
- type: Math.random() > 0.5 ? "torrent" : "usenet",
- progress,
- size: Math.floor(Math.random() * 10000) + 1,
- downSpeed: Math.floor(Math.random() * 5000),
- upSpeed: Math.floor(Math.random() * 5000),
- state: progress >= 1 ? "completed" : "downloading",
- time: 0,
- };
+ await Promise.resolve();
}
}
diff --git a/packages/integrations/src/mock/data/media-releases.ts b/packages/integrations/src/mock/data/media-releases.ts
index d820a9aa4..15504b225 100644
--- a/packages/integrations/src/mock/data/media-releases.ts
+++ b/packages/integrations/src/mock/data/media-releases.ts
@@ -13,17 +13,17 @@ export const mockMediaReleases: MediaRelease[] = [
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.",
+ "A thief who steals corporate secrets through dream-sharing technology is given the 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",
+ poster: "https://image.tmdb.org/t/p/w300/ljsZTbVsrQSqZgWeep2B1QiDKuh.jpg",
+ backdrop: "https://image.tmdb.org/t/p/w780/8ZTVqvKDQ8emSGUEMjsS4yHAwrp.jpg",
},
producer: "Warner Bros.",
price: 14.99,
rating: "8.8/10",
tags: ["Sci-Fi", "Thriller"],
- href: "https://example.com/inception",
+ href: "https://www.themoviedb.org/movie/27205-inception",
length: 148,
},
{
@@ -34,13 +34,13 @@ export const mockMediaReleases: MediaRelease[] = [
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",
+ poster: "https://image.tmdb.org/t/p/w300/ztkUQFLlC19CCMYHW73GM9hBNRh.jpg",
+ backdrop: "https://image.tmdb.org/t/p/w780/tsRy63Mu5cu8etL1X7ZLyf7UP1M.jpg",
},
producer: "AMC",
rating: "9.5/10",
tags: ["Crime", "Drama"],
- href: "https://example.com/breaking_bad",
+ href: "https://www.themoviedb.org/tv/1396-breaking-bad",
},
{
id: "3",
@@ -50,79 +50,86 @@ export const mockMediaReleases: MediaRelease[] = [
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",
+ poster: "https://image.tmdb.org/t/p/w300/mIfBmEBqA1ddSMFyMAvACvOQ6cL.jpg",
+ backdrop: "https://image.tmdb.org/t/p/w780/mIfBmEBqA1ddSMFyMAvACvOQ6cL.jpg",
},
producer: "Columbia Records",
price: 9.99,
rating: "8.5/10",
- tags: ["Electronic", "Dance", "Pop", "Funk"],
- href: "https://example.com/ram",
+ tags: ["Electronic", "Funk"],
+ href: "https://www.daftpunk.com",
},
{
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"),
+ type: "movie",
+ title: "The Dark Knight",
+ subtitle: "Theatrical Release",
+ description:
+ "Batman raises the stakes in his war on crime. With the help of Lt. Gordon and DA Harvey Dent, Batman sets out to dismantle organized crime.",
+ releaseDate: new Date("2008-07-18"),
imageUrls: {
- poster: "https://media.outnow.ch/Movies/Bilder/2025/MinecraftMovie/015.jpg",
- backdrop: "https://example.com/gatsby_backdrop.jpg",
+ poster: "https://image.tmdb.org/t/p/w300/qJ2tW6WMUDux911BTUgMe1nV8Is.jpg",
+ backdrop: "https://image.tmdb.org/t/p/w780/nMKdUUepR0i5zn0y1T4CsSB5ez.jpg",
},
- producer: "Scribner",
- price: 10.99,
- rating: "4.2/5",
- tags: ["Classic", "Fiction"],
- href: "https://example.com/gatsby",
+ producer: "Warner Bros.",
+ price: 12.99,
+ rating: "9.0/10",
+ tags: ["Action", "Crime", "Drama"],
+ href: "https://www.themoviedb.org/movie/155-the-dark-knight",
+ length: 152,
},
{
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"),
+ type: "movie",
+ title: "Interstellar",
+ subtitle: "Digital Release",
+ description: "A team of explorers travel through a wormhole in space in an attempt to ensure humanity's survival.",
+ releaseDate: new Date("2014-11-07"),
imageUrls: {
- poster: "https://media.outnow.ch/Movies/Bilder/2025/MinecraftMovie/015.jpg",
- backdrop: "https://example.com/zelda_backdrop.jpg",
+ poster: "https://image.tmdb.org/t/p/w300/gEU2QniE6E77NI6lCU6MxlNBvIx.jpg",
+ backdrop: "https://image.tmdb.org/t/p/w780/xJHokMbljXjADYdit5fK1DVfjko.jpg",
},
- producer: "Nintendo",
- price: 59.99,
- rating: "10/10",
- tags: ["Action", "Adventure"],
- href: "https://example.com/zelda",
+ producer: "Paramount",
+ price: 14.99,
+ rating: "8.7/10",
+ tags: ["Sci-Fi", "Adventure"],
+ href: "https://www.themoviedb.org/movie/157336-interstellar",
+ length: 169,
},
{
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"),
+ type: "tv",
+ title: "Severance",
+ subtitle: "S2E1 - Premiere",
+ description:
+ "Mark leads a team of office workers whose memories are surgically divided between work and personal lives.",
+ releaseDate: new Date("2025-01-17"),
imageUrls: {
- poster: "https://media.outnow.ch/Movies/Bilder/2025/MinecraftMovie/015.jpg",
- backdrop: "https://example.com/ai_healthcare_backdrop.jpg",
+ poster: "https://image.tmdb.org/t/p/w300/pJAtR4gMFnalHCmT9cByPFgkZ6B.jpg",
+ backdrop: "https://image.tmdb.org/t/p/w780/gvaAaDUbF41Z1NLuJsHPRHkv1Ie.jpg",
},
- producer: "Tech Innovations",
- rating: "4.8/5",
- tags: ["Technology", "Healthcare"],
- href: "https://example.com/ai_healthcare",
+ producer: "Apple TV+",
+ rating: "8.7/10",
+ tags: ["Thriller", "Sci-Fi"],
+ href: "https://www.themoviedb.org/tv/95396-severance",
},
{
id: "7",
- type: "video",
- title: "Wir LIEBEN unsere MAMAS | 50 Fragen zu Mamas",
- releaseDate: new Date("2024-05-18T17:00:00Z"),
+ type: "movie",
+ title: "Dune: Part Two",
+ subtitle: "Theatrical Release",
+ description:
+ "Paul Atreides unites with the Fremen to seek revenge against the conspirators who destroyed his family.",
+ releaseDate: new Date("2024-03-01"),
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",
+ poster: "https://image.tmdb.org/t/p/w300/8b8R8l88Qje9dn9OE8PY05Nez7.jpg",
+ backdrop: "https://image.tmdb.org/t/p/w780/xOMo8BRK7PfcJv9JCnx7s5hj0PX.jpg",
},
- producer: "PietSmiet",
- rating: "1K",
- tags: [],
- href: "https://www.youtube.com/watch?v=a3qyfXc1Pfg",
+ producer: "Warner Bros.",
+ price: 19.99,
+ rating: "8.5/10",
+ tags: ["Sci-Fi", "Adventure"],
+ href: "https://www.themoviedb.org/movie/693134-dune-part-two",
+ length: 166,
},
];
diff --git a/packages/integrations/src/mock/data/media-server.ts b/packages/integrations/src/mock/data/media-server.ts
index 511d47f59..96640d1ae 100644
--- a/packages/integrations/src/mock/data/media-server.ts
+++ b/packages/integrations/src/mock/data/media-server.ts
@@ -1,36 +1,95 @@
import type { IMediaServerIntegration } from "../../interfaces/media-server/media-server-integration";
import type { CurrentSessionsInput, StreamSession } from "../../interfaces/media-server/media-server-types";
-export class MediaServerMockService implements IMediaServerIntegration {
- public async getCurrentSessionsAsync(options: CurrentSessionsInput): Promise {
- return await Promise.resolve(
- Array.from({ length: 10 }, (_, index) => MediaServerMockService.createSession(index)).filter(
- (session) => !options.showOnlyPlaying || session.currentlyPlaying !== null,
- ),
- );
- }
+const sessions: StreamSession[] = [
+ {
+ sessionId: "s-0",
+ sessionName: "Living Room TV",
+ user: { userId: "u-0", username: "thomas", profilePictureUrl: null },
+ currentlyPlaying: {
+ type: "movie",
+ name: "Interstellar",
+ seasonName: undefined,
+ episodeName: null,
+ albumName: null,
+ episodeCount: null,
+ metadata: null,
+ },
+ },
+ {
+ sessionId: "s-1",
+ sessionName: "Bedroom Chromecast",
+ user: { userId: "u-1", username: "sarah", profilePictureUrl: null },
+ currentlyPlaying: {
+ type: "video",
+ name: "Breaking Bad",
+ seasonName: "Season 5",
+ episodeName: "Ozymandias",
+ albumName: null,
+ episodeCount: null,
+ metadata: null,
+ },
+ },
+ {
+ sessionId: "s-2",
+ sessionName: "Office Desktop",
+ user: { userId: "u-2", username: "mike", profilePictureUrl: null },
+ currentlyPlaying: {
+ type: "movie",
+ name: "The Dark Knight",
+ seasonName: undefined,
+ episodeName: null,
+ albumName: null,
+ episodeCount: null,
+ metadata: null,
+ },
+ },
+ {
+ sessionId: "s-3",
+ sessionName: "iPad",
+ user: { userId: "u-3", username: "emily", profilePictureUrl: null },
+ currentlyPlaying: {
+ type: "video",
+ name: "Severance",
+ seasonName: "Season 2",
+ episodeName: "The We We Are",
+ albumName: null,
+ episodeCount: null,
+ metadata: null,
+ },
+ },
+ {
+ sessionId: "s-4",
+ sessionName: "Mobile",
+ user: { userId: "u-4", username: "alex", profilePictureUrl: null },
+ currentlyPlaying: {
+ type: "audio",
+ name: "Daft Punk",
+ seasonName: undefined,
+ episodeName: null,
+ albumName: "Random Access Memories",
+ episodeCount: null,
+ metadata: null,
+ },
+ },
+ {
+ sessionId: "s-5",
+ sessionName: "Guest Room",
+ user: { userId: "u-5", username: "guest", profilePictureUrl: null },
+ currentlyPlaying: {
+ type: "movie",
+ name: "Oppenheimer",
+ seasonName: undefined,
+ episodeName: null,
+ albumName: null,
+ episodeCount: null,
+ metadata: null,
+ },
+ },
+];
- private static createSession(index: number): StreamSession {
- return {
- sessionId: `session-${index}`,
- sessionName: `Session ${index}`,
- user: {
- userId: `user-${index}`,
- username: `User${index}`,
- profilePictureUrl: "/images/mock/avatar.jpg",
- },
- currentlyPlaying:
- Math.random() > 0.9 // 10% chance of being null (not currently playing)
- ? {
- type: "movie",
- name: `Movie ${index}`,
- seasonName: undefined,
- episodeName: null,
- albumName: null,
- episodeCount: null,
- metadata: null,
- }
- : null,
- };
+export class MediaServerMockService implements IMediaServerIntegration {
+ public async getCurrentSessionsAsync(_options: CurrentSessionsInput): Promise {
+ return await Promise.resolve(sessions);
}
}
diff --git a/packages/integrations/src/mock/data/network-controller-summary.ts b/packages/integrations/src/mock/data/network-controller-summary.ts
index 7ffe80c71..59d54999a 100644
--- a/packages/integrations/src/mock/data/network-controller-summary.ts
+++ b/packages/integrations/src/mock/data/network-controller-summary.ts
@@ -4,27 +4,11 @@ import type { NetworkControllerSummary } from "../../types";
export class NetworkControllerSummaryMockService implements NetworkControllerSummaryIntegration {
public async getNetworkSummaryAsync(): Promise {
return await Promise.resolve({
- lan: {
- guests: 5,
- users: 10,
- status: "enabled",
- },
- vpn: {
- users: 3,
- status: "enabled",
- },
- wanStatus: "disabled",
- wifi: {
- status: "disabled",
- guests: 0,
- users: 0,
- },
- www: {
- latency: 22,
- status: "enabled",
- ping: 32,
- uptime: 3600,
- },
+ lan: { guests: 5, users: 18, status: "enabled" },
+ vpn: { users: 3, status: "enabled" },
+ wanStatus: "enabled",
+ wifi: { status: "enabled", guests: 2, users: 12 },
+ www: { latency: 22, status: "enabled", ping: 14, uptime: 864000 },
});
}
}
diff --git a/packages/integrations/src/mock/data/ups-summary.ts b/packages/integrations/src/mock/data/ups-summary.ts
new file mode 100644
index 000000000..36dce662a
--- /dev/null
+++ b/packages/integrations/src/mock/data/ups-summary.ts
@@ -0,0 +1,41 @@
+import type { IUpsSummaryIntegration } from "../../interfaces/ups-summary/ups-summary-integration";
+import type { UpsSummary } from "../../types";
+
+export class UpsSummaryMockService implements IUpsSummaryIntegration {
+ public async getUpsSummariesAsync(): Promise {
+ return await Promise.resolve([
+ {
+ id: "ups-rack-1",
+ name: "CyberPower OR1500",
+ manufacturer: "CyberPower",
+ model: "OR1500LCDRM1U",
+ serial: "CPS1500-001",
+ status: "online",
+ batteryCharge: 100,
+ batteryRuntime: 2280,
+ batteryVoltage: 27.3,
+ load: 42,
+ inputVoltage: 230,
+ outputVoltage: 230,
+ power: 315,
+ temperature: 29,
+ },
+ {
+ id: "ups-rack-2",
+ name: "APC Back-UPS Pro",
+ manufacturer: "APC",
+ model: "BR1500G",
+ serial: "APC1500-002",
+ status: "onBattery",
+ batteryCharge: 64,
+ batteryRuntime: 840,
+ batteryVoltage: 25.1,
+ load: 58,
+ inputVoltage: 0,
+ outputVoltage: 229,
+ power: 410,
+ temperature: 31,
+ },
+ ]);
+ }
+}
diff --git a/packages/integrations/src/mock/mock-integration.ts b/packages/integrations/src/mock/mock-integration.ts
index f04959171..b435dbd32 100644
--- a/packages/integrations/src/mock/mock-integration.ts
+++ b/packages/integrations/src/mock/mock-integration.ts
@@ -15,6 +15,7 @@ import type { IMediaServerIntegration } from "../interfaces/media-server/media-s
import type { IMediaTranscodingIntegration } from "../interfaces/media-transcoding/media-transcoding-integration";
import type { NetworkControllerSummaryIntegration } from "../interfaces/network-controller-summary/network-controller-summary-integration";
import type { ISmartHomeIntegration } from "../interfaces/smart-home/smart-home-integration";
+import type { IUpsSummaryIntegration } from "../interfaces/ups-summary/ups-summary-integration";
import { CalendarMockService } from "./data/calendar";
import { ClusterHealthMonitoringMockService } from "./data/cluster-health-monitoring";
import { DnsHoleMockService } from "./data/dns-hole";
@@ -28,6 +29,7 @@ import { NetworkControllerSummaryMockService } from "./data/network-controller-s
import { NotificationsMockService } from "./data/notifications";
import { SmartHomeMockService } from "./data/smart-home";
import { SystemHealthMonitoringMockService } from "./data/system-health-monitoring";
+import { UpsSummaryMockService } from "./data/ups-summary";
export class MockIntegration
extends Integration
@@ -43,7 +45,8 @@ export class MockIntegration
IMediaServerIntegration,
IMediaTranscodingIntegration,
NetworkControllerSummaryIntegration,
- ISmartHomeIntegration
+ ISmartHomeIntegration,
+ IUpsSummaryIntegration
{
private static readonly dnsHole = new DnsHoleMockService();
private static readonly calendar = new CalendarMockService();
@@ -58,6 +61,7 @@ export class MockIntegration
private static readonly networkController = new NetworkControllerSummaryMockService();
private static readonly notifications = new NotificationsMockService();
private static readonly smartHome = new SmartHomeMockService();
+ private static readonly upsSummary = new UpsSummaryMockService();
protected async testingAsync(_: IntegrationTestingInput): Promise {
return await Promise.resolve({
@@ -85,7 +89,7 @@ export class MockIntegration
// Health Monitoring Integrations
getSystemInfoAsync = MockIntegration.systemMonitoring.getSystemInfoAsync.bind(MockIntegration.systemMonitoring);
- getClusterInfoAsync = MockIntegration.clusterMonitoring.getClusterInfoAsync.bind(MockIntegration.downloadClient);
+ getClusterInfoAsync = MockIntegration.clusterMonitoring.getClusterInfoAsync.bind(MockIntegration.clusterMonitoring);
// IndexerManagerIntegration
getIndexersAsync = MockIntegration.indexerManager.getIndexersAsync.bind(MockIntegration.indexerManager);
@@ -104,7 +108,7 @@ export class MockIntegration
declineRequestAsync = MockIntegration.mediaRequest.declineRequestAsync.bind(MockIntegration.mediaRequest);
// MediaServerIntegration
- getCurrentSessionsAsync = MockIntegration.mediaServer.getCurrentSessionsAsync.bind(MockIntegration.mediaRequest);
+ getCurrentSessionsAsync = MockIntegration.mediaServer.getCurrentSessionsAsync.bind(MockIntegration.mediaServer);
// MediaTranscodingIntegration
getStatisticsAsync = MockIntegration.mediaTranscoding.getStatisticsAsync.bind(MockIntegration.mediaTranscoding);
@@ -123,4 +127,7 @@ export class MockIntegration
getEntityStateAsync = MockIntegration.smartHome.getEntityStateAsync.bind(MockIntegration.smartHome);
triggerAutomationAsync = MockIntegration.smartHome.triggerAutomationAsync.bind(MockIntegration.smartHome);
triggerToggleAsync = MockIntegration.smartHome.triggerToggleAsync.bind(MockIntegration.smartHome);
+
+ // UpsSummaryIntegration
+ getUpsSummariesAsync = MockIntegration.upsSummary.getUpsSummariesAsync.bind(MockIntegration.upsSummary);
}
diff --git a/packages/integrations/src/overseerr/overseerr-integration.ts b/packages/integrations/src/overseerr/overseerr-integration.ts
index 619af2541..643d2ac60 100644
--- a/packages/integrations/src/overseerr/overseerr-integration.ts
+++ b/packages/integrations/src/overseerr/overseerr-integration.ts
@@ -60,6 +60,9 @@ export class OverseerrIntegration
text: "overview" in result ? result.overview : undefined,
type: result.mediaType,
inLibrary: result.mediaInfo !== undefined,
+ availability: result.mediaInfo
+ ? this.mapAvailability(result.mediaInfo.status as UpstreamMediaAvailability, false)
+ : undefined,
}));
}
@@ -70,7 +73,12 @@ export class OverseerrIntegration
"X-Api-Key": this.getSecretValue("apiKey"),
},
});
- return await mediaInformationSchema.parseAsync(await response.json());
+ const data = await mediaInformationSchema.parseAsync(await response.json());
+ const requestedSeasons = [
+ ...new Set(data.mediaInfo?.requests?.flatMap((req) => req.seasons.map((s) => s.seasonNumber)) ?? []),
+ ];
+ const { mediaInfo: _strip, ...rest } = data;
+ return { ...rest, requestedSeasons };
}
/**
@@ -341,6 +349,22 @@ interface MovieInformation {
releaseDate: string;
}
+const mediaInfoRequestsSchema = z
+ .object({
+ requests: z
+ .array(
+ z.object({
+ seasons: z.array(
+ z.object({
+ seasonNumber: z.number(),
+ }),
+ ),
+ }),
+ )
+ .optional(),
+ })
+ .optional();
+
const mediaInformationSchema = z.union([
z.object({
id: z.number(),
@@ -355,14 +379,22 @@ const mediaInformationSchema = z.union([
),
numberOfSeasons: z.number(),
posterPath: z.string().startsWith("/"),
+ mediaInfo: mediaInfoRequestsSchema,
}),
z.object({
id: z.number(),
overview: z.string(),
posterPath: z.string().startsWith("/"),
+ mediaInfo: mediaInfoRequestsSchema,
}),
]);
+const searchMediaInfoSchema = z
+ .object({
+ status: z.number(),
+ })
+ .optional();
+
const searchSchema = z.object({
results: z
.array(
@@ -373,7 +405,7 @@ const searchSchema = z.object({
name: z.string(),
posterPath: z.string().startsWith("/").endsWith(".jpg").nullable(),
overview: z.string(),
- mediaInfo: z.object({}).optional(),
+ mediaInfo: searchMediaInfoSchema,
}),
z.object({
id: z.number(),
@@ -381,14 +413,14 @@ const searchSchema = z.object({
title: z.string(),
posterPath: z.string().startsWith("/").endsWith(".jpg").nullable(),
overview: z.string(),
- mediaInfo: z.object({}).optional(),
+ mediaInfo: searchMediaInfoSchema,
}),
z.object({
id: z.number(),
mediaType: z.literal("person"),
name: z.string(),
profilePath: z.string().startsWith("/").endsWith(".jpg").nullable(),
- mediaInfo: z.object({}).optional(),
+ mediaInfo: searchMediaInfoSchema,
}),
]),
)
diff --git a/packages/integrations/src/peanut/peanut-integration.ts b/packages/integrations/src/peanut/peanut-integration.ts
new file mode 100644
index 000000000..ad3fe6296
--- /dev/null
+++ b/packages/integrations/src/peanut/peanut-integration.ts
@@ -0,0 +1,103 @@
+import { ResponseError } from "@homarr/common/server";
+import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
+
+import type { IntegrationTestingInput } from "../base/integration";
+import { Integration } from "../base/integration";
+import { TestConnectionError } from "../base/test-connection/test-connection-error";
+import type { TestingResult } from "../base/test-connection/test-connection-service";
+import type { IUpsSummaryIntegration } from "../interfaces/ups-summary/ups-summary-integration";
+import type { UpsStatus, UpsSummary } from "../interfaces/ups-summary/ups-summary-types";
+import type { PeaNutDevice } from "./peanut-types";
+import { peaNutDevicesSchema } from "./peanut-types";
+
+export class PeaNutIntegration extends Integration implements IUpsSummaryIntegration {
+ protected async testingAsync(input: IntegrationTestingInput): Promise {
+ const response = await input.fetchAsync(this.url("/api/v1/devices"), {
+ headers: this.getAuthHeaders(),
+ });
+
+ if (!response.ok) return TestConnectionError.StatusResult(response);
+
+ await response.json();
+
+ return { success: true };
+ }
+
+ public async getUpsSummariesAsync(): Promise {
+ const response = await fetchWithTrustedCertificatesAsync(this.url("/api/v1/devices", { meta: true }), {
+ headers: this.getAuthHeaders(),
+ });
+
+ if (!response.ok) {
+ throw new ResponseError(response);
+ }
+
+ const devices = peaNutDevicesSchema.parse(await response.json());
+ return devices.map((device, index) => this.mapDeviceToSummary(device, index));
+ }
+
+ private mapDeviceToSummary(device: PeaNutDevice, index: number): UpsSummary {
+ const manufacturer = this.readString(device, "device.mfr") ?? this.readString(device, "ups.mfr");
+ const model = this.readString(device, "device.model") ?? this.readString(device, "ups.model");
+ const id = this.readString(device, "peanut.device_id") ?? `ups-${index}`;
+ const name = [manufacturer, model].filter(Boolean).join(" ") || id;
+
+ return {
+ id,
+ name,
+ manufacturer,
+ model,
+ serial: this.readString(device, "device.serial"),
+ status: this.parseStatus(this.readString(device, "ups.status")),
+ batteryCharge: this.readNumber(device, "battery.charge"),
+ batteryRuntime: this.readNumber(device, "battery.runtime"),
+ batteryVoltage: this.readNumber(device, "battery.voltage"),
+ load: this.readNumber(device, "ups.load"),
+ inputVoltage: this.readNumber(device, "input.voltage"),
+ outputVoltage: this.readNumber(device, "output.voltage"),
+ power: this.readNumber(device, "ups.realpower") ?? this.readNumber(device, "ups.power"),
+ temperature: this.readNumber(device, "ups.temperature"),
+ };
+ }
+
+ /**
+ * Resolves the raw NUT `ups.status` flags (e.g. "OL CHRG", "OB DISCHRG LB") into a single
+ * normalized status. The flags are checked in priority order so the most critical state wins.
+ */
+ private parseStatus(rawStatus: string | null): UpsStatus {
+ const flags = (rawStatus ?? "").toUpperCase().split(/\s+/).filter(Boolean);
+
+ if (flags.includes("LB")) return "lowBattery";
+ if (flags.includes("OB")) return "onBattery";
+ if (flags.includes("OL")) {
+ return flags.includes("CHRG") ? "charging" : "online";
+ }
+
+ return "unknown";
+ }
+
+ private readString(device: PeaNutDevice, key: string): string | null {
+ const value = device[key];
+ if (value === undefined || value === null) return null;
+ const stringValue = String(value).trim();
+ return stringValue.length > 0 ? stringValue : null;
+ }
+
+ private readNumber(device: PeaNutDevice, key: string): number | null {
+ const value = device[key];
+ if (value === undefined || value === null || value === "") return null;
+ const numberValue = typeof value === "number" ? value : Number(value);
+ return Number.isFinite(numberValue) ? numberValue : null;
+ }
+
+ private getAuthHeaders(): Record {
+ if (this.hasSecretValue("username") && this.hasSecretValue("password")) {
+ const credentials = Buffer.from(`${this.getSecretValue("username")}:${this.getSecretValue("password")}`).toString(
+ "base64",
+ );
+ return { Authorization: `Basic ${credentials}` };
+ }
+
+ return {};
+ }
+}
diff --git a/packages/integrations/src/peanut/peanut-types.ts b/packages/integrations/src/peanut/peanut-types.ts
new file mode 100644
index 000000000..fab0626f0
--- /dev/null
+++ b/packages/integrations/src/peanut/peanut-types.ts
@@ -0,0 +1,10 @@
+import { z } from "zod/v4";
+
+// PeaNUT returns each device as a flat map of NUT variables (e.g. "ups.status",
+// "battery.charge"). Values are either strings or numbers depending on the
+// variable, so we accept both and coerce them when mapping to a UpsSummary.
+export const peaNutDeviceSchema = z.record(z.string(), z.union([z.string(), z.number()]));
+
+export const peaNutDevicesSchema = z.array(peaNutDeviceSchema);
+
+export type PeaNutDevice = z.infer;
diff --git a/packages/integrations/src/peanut/test/peanut-integration.spec.ts b/packages/integrations/src/peanut/test/peanut-integration.spec.ts
new file mode 100644
index 000000000..0fdb8e460
--- /dev/null
+++ b/packages/integrations/src/peanut/test/peanut-integration.spec.ts
@@ -0,0 +1,166 @@
+import { Response } from "undici";
+import { beforeEach, describe, expect, test, vi } from "vitest";
+
+import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
+
+import type { IntegrationSecret } from "../../base/types";
+import { PeaNutIntegration } from "../peanut-integration";
+
+vi.mock("@homarr/core/infrastructure/http", () => ({
+ fetchWithTrustedCertificatesAsync: vi.fn(),
+}));
+
+const TEST_URL = "https://peanut.example.com";
+
+const mockFetch = vi.mocked(fetchWithTrustedCertificatesAsync);
+
+const mockDevicesResponse = (devices: Record[]) => {
+ mockFetch.mockImplementation(
+ () =>
+ Promise.resolve(
+ new Response(JSON.stringify(devices), {
+ status: 200,
+ headers: { "content-type": "application/json" },
+ }),
+ ) as unknown as ReturnType,
+ );
+};
+
+const createIntegration = (decryptedSecrets: IntegrationSecret[] = []) =>
+ new PeaNutIntegration({
+ id: "test-peanut",
+ name: "Test PeaNUT",
+ url: TEST_URL,
+ externalUrl: null,
+ decryptedSecrets,
+ });
+
+beforeEach(() => {
+ mockFetch.mockReset();
+});
+
+describe("PeaNutIntegration getUpsSummariesAsync", () => {
+ test("maps NUT variables to a UPS summary and coerces string values to numbers", async () => {
+ mockDevicesResponse([
+ {
+ "device.mfr": "CyberPower",
+ "device.model": "CP1500EPFCLCD",
+ "device.serial": "ABC123",
+ "ups.status": "OL CHRG",
+ "ups.load": "23",
+ "ups.realpower": "120",
+ "ups.temperature": "31.5",
+ "battery.charge": "100",
+ "battery.runtime": "3720",
+ "battery.voltage": "27.3",
+ "input.voltage": "230",
+ "output.voltage": "229",
+ "peanut.device_id": "ups",
+ "peanut.server": "localhost:3493",
+ },
+ ]);
+
+ const [summary] = await createIntegration().getUpsSummariesAsync();
+
+ expect(summary).toStrictEqual({
+ id: "ups",
+ name: "CyberPower CP1500EPFCLCD",
+ manufacturer: "CyberPower",
+ model: "CP1500EPFCLCD",
+ serial: "ABC123",
+ status: "charging",
+ batteryCharge: 100,
+ batteryRuntime: 3720,
+ batteryVoltage: 27.3,
+ load: 23,
+ inputVoltage: 230,
+ outputVoltage: 229,
+ power: 120,
+ temperature: 31.5,
+ });
+ });
+
+ test("falls back when optional variables are missing", async () => {
+ mockDevicesResponse([
+ {
+ "ups.status": "OB DISCHRG LB",
+ "ups.power": "90",
+ "battery.charge": "15",
+ },
+ ]);
+
+ const [summary] = await createIntegration().getUpsSummariesAsync();
+
+ expect(summary).toMatchObject({
+ id: "ups-0",
+ name: "ups-0",
+ manufacturer: null,
+ model: null,
+ status: "lowBattery",
+ batteryCharge: 15,
+ batteryRuntime: null,
+ load: null,
+ inputVoltage: null,
+ outputVoltage: null,
+ power: 90,
+ temperature: null,
+ });
+ });
+
+ test("returns a summary per device", async () => {
+ mockDevicesResponse([
+ { "peanut.device_id": "ups-a", "ups.status": "OL" },
+ { "peanut.device_id": "ups-b", "ups.status": "OB" },
+ ]);
+
+ const summaries = await createIntegration().getUpsSummariesAsync();
+
+ expect(summaries.map((summary) => summary.id)).toStrictEqual(["ups-a", "ups-b"]);
+ });
+
+ test("normalizes NUT status flags into a typed status", async () => {
+ mockDevicesResponse([
+ { "ups.status": "OL" },
+ { "ups.status": "OL CHRG" },
+ { "ups.status": "OB DISCHRG" },
+ { "ups.status": "OB DISCHRG LB" },
+ { "ups.status": "OL LB" },
+ { "ups.status": "" },
+ ]);
+
+ const summaries = await createIntegration().getUpsSummariesAsync();
+
+ expect(summaries.map((summary) => summary.status)).toStrictEqual([
+ "online",
+ "charging",
+ "onBattery",
+ "lowBattery",
+ "lowBattery",
+ "unknown",
+ ]);
+ });
+});
+
+describe("PeaNutIntegration authentication", () => {
+ test("sends a Basic auth header when username and password are configured", async () => {
+ mockDevicesResponse([]);
+
+ await createIntegration([
+ { kind: "username", value: "admin" },
+ { kind: "password", value: "secret" },
+ ]).getUpsSummariesAsync();
+
+ const expected = `Basic ${Buffer.from("admin:secret").toString("base64")}`;
+ const [, requestInit] = mockFetch.mock.calls[0] ?? [];
+ expect(requestInit?.headers).toMatchObject({ Authorization: expected });
+ });
+
+ test("omits the auth header when no credentials are configured", async () => {
+ mockDevicesResponse([]);
+
+ await createIntegration().getUpsSummariesAsync();
+
+ const [, requestInit] = mockFetch.mock.calls[0] ?? [];
+ expect(requestInit?.headers).toStrictEqual({});
+ });
+});
diff --git a/packages/integrations/src/types.ts b/packages/integrations/src/types.ts
index 10ae96ee4..fcb8a77ae 100644
--- a/packages/integrations/src/types.ts
+++ b/packages/integrations/src/types.ts
@@ -3,6 +3,7 @@ export * from "./interfaces/dns-hole-summary/dns-hole-summary-types";
export * from "./interfaces/network-controller-summary/network-controller-summary-types";
export * from "./interfaces/firewall-summary/firewall-summary-types";
export * from "./interfaces/health-monitoring/health-monitoring-types";
+export * from "./interfaces/ups-summary/ups-summary-types";
export * from "./interfaces/indexer-manager/indexer-manager-types";
export * from "./interfaces/media-requests/media-request-types";
export * from "./base/searchable-integration";
diff --git a/packages/modals-collection/src/apps/app-select-modal.tsx b/packages/modals-collection/src/apps/app-select-modal.tsx
index e5530d341..6ac726ac0 100644
--- a/packages/modals-collection/src/apps/app-select-modal.tsx
+++ b/packages/modals-collection/src/apps/app-select-modal.tsx
@@ -1,11 +1,12 @@
import { useMemo, useState } from "react";
-import { Button, Card, Center, Grid, Input, Stack, Text } from "@mantine/core";
-import { IconPlus, IconSearch } from "@tabler/icons-react";
+import { Box, Button, Card, Center, Group, Stack, Text, Tooltip } from "@mantine/core";
+import { IconPlus } from "@tabler/icons-react";
import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
-import { createModal, useModalAction } from "@homarr/modals";
+import { createModal, modalSizeSelect, useModalAction } from "@homarr/modals";
import { useI18n } from "@homarr/translation/client";
+import { SelectGridLayout, selectGridCardHeight } from "@homarr/ui";
import { QuickAddAppModal } from "./quick-add-app/quick-add-app-modal";
@@ -29,96 +30,119 @@ export const AppSelectModal = createModal(({ actions, inner
);
const handleSelect = (app: RouterOutputs["app"]["selectable"][number]) => {
- if (innerProps.onSelect) {
- innerProps.onSelect(app);
- }
+ innerProps.onSelect?.(app);
actions.closeModal();
};
const handleAddNewApp = () => {
openQuickAddAppModal({
onClose(app) {
- if (innerProps.onSelect) {
- innerProps.onSelect(app);
- }
+ innerProps.onSelect?.(app);
actions.closeModal();
},
});
};
return (
-
- setSearch(event.currentTarget.value)}
- leftSection={}
- placeholder={`${t("app.action.select.search")}...`}
- data-autofocus
- onKeyDown={(event) => {
- if (event.key === "Enter" && filteredApps.length === 1 && filteredApps[0]) {
- handleSelect(filteredApps[0]);
- }
- }}
- />
+ {
+ if (event.key === "Enter" && filteredApps.length === 1 && filteredApps[0]) {
+ handleSelect(filteredApps[0]);
+ }
+ }}
+ >
+ {innerProps.withCreate && (
+ e.currentTarget.style.setProperty("--_hover-opacity", "1")}
+ onMouseLeave={(e) => e.currentTarget.style.setProperty("--_hover-opacity", "0")}
+ >
+
+
+
+
+ {t("app.action.create.title")}
+
+
+
+ {t("app.action.create.description")}
+
+
+
+
+
+
+ )}
-
- {innerProps.withCreate && (
-
-
-
-
-
-
-
-
- {t("app.action.create.title")}
-
-
- {t("app.action.create.description")}
-
-
-
-
-
-
- )}
+ {filteredApps.map((app) => (
+ e.currentTarget.style.setProperty("--_hover-opacity", "1")}
+ onMouseLeave={(e) => e.currentTarget.style.setProperty("--_hover-opacity", "0")}
+ >
+
+
+
+
+ {app.name}
+
+
+
+
+ {app.description ?? ""}
+
+
+
+
+
+
+
+ ))}
- {filteredApps.map((app) => (
-
-
-
-
-
-
-
-
- {app.name}
-
-
- {app.description ?? ""}
-
-
-
-
-
-
- ))}
-
- {filteredApps.length === 0 && !isPending && (
-
-
- {t("app.action.select.noResults")}
-
-
- )}
-
-
+ {filteredApps.length === 0 && !isPending && (
+
+ {t("app.action.select.noResults")}
+
+ )}
+
);
}).withOptions({
defaultTitle: (t) => t("app.action.select.title"),
- size: "xl",
+ size: modalSizeSelect,
});
diff --git a/packages/modals-collection/src/apps/quick-add-app/quick-add-app-modal.tsx b/packages/modals-collection/src/apps/quick-add-app/quick-add-app-modal.tsx
index 617bf6403..04e9f6f8b 100644
--- a/packages/modals-collection/src/apps/quick-add-app/quick-add-app-modal.tsx
+++ b/packages/modals-collection/src/apps/quick-add-app/quick-add-app-modal.tsx
@@ -3,7 +3,7 @@ import type { z } from "zod/v4";
import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
import { AppForm } from "@homarr/forms-collection";
-import { createModal } from "@homarr/modals";
+import { createModal, modalSizeForm } from "@homarr/modals";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n, useScopedI18n } from "@homarr/translation/client";
import type { appManageSchema } from "@homarr/validation/app";
@@ -54,4 +54,5 @@ export const QuickAddAppModal = createModal(({ actions, i
defaultTitle(t) {
return t("board.action.quickCreateApp.modal.title");
},
+ size: modalSizeForm,
});
diff --git a/packages/modals-collection/src/search-engines/request-media-modal.tsx b/packages/modals-collection/src/search-engines/request-media-modal.tsx
index 89306f3af..c75647969 100644
--- a/packages/modals-collection/src/search-engines/request-media-modal.tsx
+++ b/packages/modals-collection/src/search-engines/request-media-modal.tsx
@@ -1,6 +1,6 @@
import { useMemo } from "react";
-import { Button, Group, Image, LoadingOverlay, Stack, Text } from "@mantine/core";
-import type { MRT_ColumnDef } from "mantine-react-table";
+import { Badge, Button, Group, Image, LoadingOverlay, Stack, Text } from "@mantine/core";
+import type { MRT_ColumnDef, MRT_Row } from "mantine-react-table";
import { MRT_Table } from "mantine-react-table";
import { clientApi } from "@homarr/api/client";
@@ -16,13 +16,13 @@ interface RequestMediaModalProps {
}
export const RequestMediaModal = createModal(({ actions, innerProps }) => {
- const { data, isPending: isPendingQuery } = clientApi.searchEngine.getMediaRequestOptions.useQuery({
+ const { data, isPending: isPendingQuery } = clientApi.integration.getMediaRequestOptions.useQuery({
integrationId: innerProps.integrationId,
mediaId: innerProps.mediaId,
mediaType: innerProps.mediaType,
});
- const { mutate, isPending: isPendingMutation } = clientApi.searchEngine.requestMedia.useMutation({
+ const { mutate, isPending: isPendingMutation } = clientApi.integration.requestMedia.useMutation({
onSuccess() {
actions.closeModal();
showSuccessNotification({
@@ -33,6 +33,7 @@ export const RequestMediaModal = createModal(({ actions,
const isPending = isPendingQuery || isPendingMutation;
const t = useI18n();
+ const requestedSeasons = new Set(data?.requestedSeasons ?? []);
const columns = useMemo[]>(
() => [
@@ -44,8 +45,18 @@ export const RequestMediaModal = createModal(({ actions,
accessorKey: "episodeCount",
header: t("search.engine.media.request.modal.table.header.episodes"),
},
+ {
+ id: "status",
+ header: t("search.engine.media.request.modal.table.header.status"),
+ Cell: ({ row }) =>
+ requestedSeasons.has(row.original.seasonNumber) ? (
+
+ {t("search.engine.media.request.modal.table.status.requested")}
+
+ ) : null,
+ },
],
- [],
+ [data?.requestedSeasons],
);
const table = useTranslatedMantineReactTable({
@@ -56,7 +67,7 @@ export const RequestMediaModal = createModal(({ actions,
enablePagination: false,
enableSorting: false,
enableSelectAll: true,
- enableRowSelection: true,
+ enableRowSelection: (row: MRT_Row) => !requestedSeasons.has(row.original.seasonNumber),
mantineTableProps: {
highlightOnHover: false,
striped: "odd",
diff --git a/packages/modals/index.ts b/packages/modals/index.ts
index f7a07beae..196451a8e 100644
--- a/packages/modals/index.ts
+++ b/packages/modals/index.ts
@@ -1,2 +1,4 @@
export { ModalProvider, useModalAction, useConfirmModal } from "./src";
export { createModal } from "./src/creator";
+export { ModalFormFooter } from "./src/modal-form-footer";
+export { modalSizeForm, modalSizeSelect } from "./src/modal-sizes";
diff --git a/packages/modals/src/modal-form-footer.tsx b/packages/modals/src/modal-form-footer.tsx
new file mode 100644
index 000000000..f274b2dd4
--- /dev/null
+++ b/packages/modals/src/modal-form-footer.tsx
@@ -0,0 +1,53 @@
+"use client";
+
+import type { ReactNode } from "react";
+import { Button, Group } from "@mantine/core";
+
+import { useI18n } from "@homarr/translation/client";
+
+interface ModalFormFooterProps {
+ onCancel: () => void;
+ submitLabel?: string;
+ cancelLabel?: string;
+ loading?: boolean;
+ leftSection?: ReactNode;
+}
+
+const ActionButtons = ({
+ onCancel,
+ submitLabel,
+ cancelLabel,
+ loading,
+}: Pick) => {
+ const t = useI18n();
+
+ return (
+ <>
+
+
+ >
+ );
+};
+
+export const ModalFormFooter = ({ onCancel, submitLabel, cancelLabel, loading, leftSection }: ModalFormFooterProps) => {
+ const actionButtons = (
+
+ );
+
+ if (leftSection) {
+ return (
+
+ {leftSection}
+
+ {actionButtons}
+
+
+ );
+ }
+
+ return {actionButtons};
+};
diff --git a/packages/modals/src/modal-sizes.ts b/packages/modals/src/modal-sizes.ts
new file mode 100644
index 000000000..cf631943c
--- /dev/null
+++ b/packages/modals/src/modal-sizes.ts
@@ -0,0 +1 @@
+export { modalSizeForm, modalSizeSelect } from "@homarr/ui";
diff --git a/packages/notifications/src/index.tsx b/packages/notifications/src/index.tsx
index 071496b20..41bb16964 100644
--- a/packages/notifications/src/index.tsx
+++ b/packages/notifications/src/index.tsx
@@ -1,6 +1,6 @@
import type { NotificationData } from "@mantine/notifications";
import { notifications } from "@mantine/notifications";
-import { IconCheck, IconX } from "@tabler/icons-react";
+import { IconCheck, IconInfoCircle, IconX } from "@tabler/icons-react";
export const showSuccessNotification = (props: NotificationData) =>
notifications.show({
@@ -15,3 +15,10 @@ export const showErrorNotification = (props: NotificationData) =>
color: "red",
icon: ,
});
+
+export const showWarningNotification = (props: NotificationData) =>
+ notifications.show({
+ ...props,
+ color: "yellow",
+ icon: ,
+ });
diff --git a/packages/old-import/src/components/initial-oldmarr-import.tsx b/packages/old-import/src/components/initial-oldmarr-import.tsx
index a87a8f651..92aa301a5 100644
--- a/packages/old-import/src/components/initial-oldmarr-import.tsx
+++ b/packages/old-import/src/components/initial-oldmarr-import.tsx
@@ -69,7 +69,7 @@ export const InitialOldmarrImport = ({ file, analyseResult }: InitialOldmarrImpo
};
return (
-
+
{
diff --git a/packages/old-import/src/components/initial/board-selection-card.tsx b/packages/old-import/src/components/initial/board-selection-card.tsx
index 4665ec2cd..6f178f669 100644
--- a/packages/old-import/src/components/initial/board-selection-card.tsx
+++ b/packages/old-import/src/components/initial/board-selection-card.tsx
@@ -38,7 +38,7 @@ export const BoardSelectionCard = ({ selections, updateSelections }: BoardSelect
}
return (
-
+
diff --git a/packages/old-import/src/components/initial/import-settings-card.tsx b/packages/old-import/src/components/initial/import-settings-card.tsx
index 1a2809d76..f230ff921 100644
--- a/packages/old-import/src/components/initial/import-settings-card.tsx
+++ b/packages/old-import/src/components/initial/import-settings-card.tsx
@@ -17,7 +17,7 @@ interface ImportSettingsCardProps {
export const ImportSettingsCard = ({ settings, updateSetting }: ImportSettingsCardProps) => {
const tImportSettings = useScopedI18n("init.step.import.importSettings");
return (
-
+
{tImportSettings("title")}
diff --git a/packages/old-import/src/components/initial/import-summary-card.tsx b/packages/old-import/src/components/initial/import-summary-card.tsx
index 9993b3ac2..1000b35a6 100644
--- a/packages/old-import/src/components/initial/import-summary-card.tsx
+++ b/packages/old-import/src/components/initial/import-summary-card.tsx
@@ -13,7 +13,7 @@ interface ImportSummaryCardProps {
export const ImportSummaryCard = ({ counts, onSubmit, loading }: ImportSummaryCardProps) => {
const tSummary = useScopedI18n("init.step.import.summary");
return (
-
+
{tSummary("title")}
diff --git a/packages/old-import/src/widgets/options.ts b/packages/old-import/src/widgets/options.ts
index 32b1f25c3..b407d0e45 100644
--- a/packages/old-import/src/widgets/options.ts
+++ b/packages/old-import/src/widgets/options.ts
@@ -44,6 +44,7 @@ const optionMapping: OptionMapping = {
hideIcon: (oldOptions) => oldOptions.items.some((item) => item.hideIcon),
hideHostname: (oldOptions) => oldOptions.items.some((item) => item.hideHostname),
openNewTab: (oldOptions) => oldOptions.items.some((item) => item.openNewTab),
+ withBorder: () => undefined,
},
calendar: {
releaseType: (oldOptions) => [oldOptions.radarrReleaseType],
diff --git a/packages/request-handler/src/docker-log-decode.spec.ts b/packages/request-handler/src/docker-log-decode.spec.ts
new file mode 100644
index 000000000..b82dfd833
--- /dev/null
+++ b/packages/request-handler/src/docker-log-decode.spec.ts
@@ -0,0 +1,53 @@
+import { describe, expect, test } from "vitest";
+
+import { createDockerLogStreamProcessor, decodeDockerLogs } from "./docker-log-decode";
+
+const encodeDockerLogFrame = (streamType: number, message: string) => {
+ const payload = Buffer.from(message, "utf-8");
+ const header = Buffer.alloc(8);
+ header.writeUInt8(streamType, 0);
+ header.writeUInt32BE(payload.length, 4);
+ return Buffer.concat([header, payload]);
+};
+
+describe("decodeDockerLogs", () => {
+ test("decodes multiplexed docker log frames", () => {
+ const logs = Buffer.concat([encodeDockerLogFrame(1, "hello "), encodeDockerLogFrame(2, "world")]);
+
+ expect(decodeDockerLogs(logs)).toBe("hello world");
+ });
+
+ test("returns plain text logs unchanged", () => {
+ expect(decodeDockerLogs("plain text log")).toBe("plain text log");
+ });
+
+ test("falls back to raw buffer text for non-multiplexed output", () => {
+ expect(decodeDockerLogs(Buffer.from("raw docker output", "utf-8"))).toBe("raw docker output");
+ });
+});
+
+describe("createDockerLogStreamProcessor", () => {
+ test("emits decoded chunks from a stream", () => {
+ const messages: string[] = [];
+ const processChunk = createDockerLogStreamProcessor(
+ (data) => messages.push(data),
+ () => undefined,
+ 1024,
+ );
+
+ const chunk = encodeDockerLogFrame(1, "streamed log");
+ expect(processChunk(chunk)).toBe(true);
+ expect(messages).toEqual(["streamed log"]);
+ });
+
+ test("returns false when message size exceeds limit", () => {
+ const processChunk = createDockerLogStreamProcessor(
+ () => undefined,
+ () => undefined,
+ 4,
+ );
+
+ const chunk = encodeDockerLogFrame(1, "this message is too long");
+ expect(processChunk(chunk)).toBe(false);
+ });
+});
diff --git a/packages/request-handler/src/docker-log-decode.ts b/packages/request-handler/src/docker-log-decode.ts
new file mode 100644
index 000000000..a069091d6
--- /dev/null
+++ b/packages/request-handler/src/docker-log-decode.ts
@@ -0,0 +1,89 @@
+const DOCKER_STREAM_HEADER_SIZE = 8;
+const DOCKER_STREAM_TYPES = new Set([1, 2]);
+
+export const decodeDockerLogs = (logs: Buffer | string) => {
+ if (typeof logs === "string") {
+ return logs;
+ }
+
+ if (logs.length === 0) {
+ return "";
+ }
+
+ let cursor = 0;
+ const parts: string[] = [];
+
+ while (cursor < logs.length) {
+ if (cursor + DOCKER_STREAM_HEADER_SIZE > logs.length) {
+ break;
+ }
+
+ const streamType = logs.readUInt8(cursor);
+ if (!DOCKER_STREAM_TYPES.has(streamType)) {
+ return logs.toString("utf-8");
+ }
+
+ const length = logs.readUInt32BE(cursor + 4);
+ const start = cursor + DOCKER_STREAM_HEADER_SIZE;
+ const end = start + length;
+ if (end > logs.length) {
+ break;
+ }
+
+ parts.push(logs.subarray(start, end).toString("utf-8"));
+ cursor = end;
+ }
+
+ const decoded = parts.join("");
+ if (decoded.length === 0) {
+ return logs.toString("utf-8");
+ }
+
+ return decoded;
+};
+
+export const createDockerLogStreamProcessor = (
+ onData: (data: string) => void,
+ onError: (err: Error) => void,
+ maxMessageSize: number,
+) => {
+ let buffer = Buffer.alloc(0);
+
+ const processChunk = (chunk: Buffer) => {
+ buffer = Buffer.concat([buffer, chunk]);
+
+ while (buffer.length > 0) {
+ if (buffer.length < DOCKER_STREAM_HEADER_SIZE) {
+ break;
+ }
+
+ const streamType = buffer.readUInt8(0);
+ if (!DOCKER_STREAM_TYPES.has(streamType)) {
+ onData(buffer.toString("utf-8"));
+ buffer = Buffer.alloc(0);
+ break;
+ }
+
+ const length = buffer.readUInt32BE(4);
+ if (length > maxMessageSize) {
+ onError(
+ new Error(`Docker log message size (${length} bytes) exceeds maximum allowed (${maxMessageSize} bytes)`),
+ );
+ return false;
+ }
+
+ const fullMessageSize = DOCKER_STREAM_HEADER_SIZE + length;
+ if (buffer.length < fullMessageSize) {
+ break;
+ }
+
+ const payload = buffer.subarray(DOCKER_STREAM_HEADER_SIZE, fullMessageSize);
+ onData(payload.toString("utf-8"));
+ buffer = buffer.subarray(fullMessageSize);
+ }
+
+ return true;
+ };
+
+ return processChunk;
+};
diff --git a/packages/request-handler/src/docker.ts b/packages/request-handler/src/docker.ts
index a31eed56c..2d228a598 100644
--- a/packages/request-handler/src/docker.ts
+++ b/packages/request-handler/src/docker.ts
@@ -1,5 +1,6 @@
+import type { Readable } from "node:stream";
import dayjs from "dayjs";
-import type { ContainerInfo, ContainerStats } from "dockerode";
+import type { Container, ContainerInfo, ContainerStats } from "dockerode";
import type Dockerode from "dockerode";
import { bestMatch } from "@homarr/common";
@@ -7,23 +8,260 @@ import { createLogger } from "@homarr/core/infrastructure/logs";
import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error";
import { db, like, or } from "@homarr/db";
import { icons } from "@homarr/db/schema";
-import type { ContainerState } from "@homarr/docker";
+import { extractContainerImageName } from "@homarr/definitions";
+import type { ContainerState, Port } from "@homarr/docker";
import { dockerLabels, DockerSingleton } from "@homarr/docker";
+import { createDockerLogStreamProcessor, decodeDockerLogs } from "./docker-log-decode";
import { createCachedWidgetRequestHandler } from "./lib/cached-widget-request-handler";
const logger = createLogger({ module: "dockerRequestHandler" });
+const isDemoMode = ["1", "yes", "t", "true"].includes((process.env.DEMO_MODE ?? "").toLowerCase());
+
+const port = (privatePort: number, publicPort: number, type: string): Port => ({
+ IP: "0.0.0.0",
+ PrivatePort: privatePort,
+ PublicPort: publicPort,
+ Type: type,
+});
+
+const mockContainers: {
+ id: string;
+ name: string;
+ host: string;
+ state: ContainerState;
+ image: string;
+ iconUrl: string;
+ cpuUsage: number;
+ memoryUsage: number;
+ ports: Port[];
+}[] = [
+ {
+ id: "a1b2c3d4e5f6",
+ name: "sonarr",
+ host: "local",
+ state: "running",
+ image: "lscr.io/linuxserver/sonarr:latest",
+ iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/sonarr.svg",
+ cpuUsage: 2.3,
+ memoryUsage: 256 * 1024 * 1024,
+ ports: [port(8989, 8989, "tcp")],
+ },
+ {
+ id: "b2c3d4e5f6a7",
+ name: "radarr",
+ host: "local",
+ state: "running",
+ image: "lscr.io/linuxserver/radarr:latest",
+ iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/radarr.svg",
+ cpuUsage: 1.8,
+ memoryUsage: 220 * 1024 * 1024,
+ ports: [port(7878, 7878, "tcp")],
+ },
+ {
+ id: "c3d4e5f6a7b8",
+ name: "plex",
+ host: "local",
+ state: "running",
+ image: "lscr.io/linuxserver/plex:latest",
+ iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/plex.svg",
+ cpuUsage: 12.5,
+ memoryUsage: 1024 * 1024 * 1024,
+ ports: [port(32400, 32400, "tcp")],
+ },
+ {
+ id: "d4e5f6a7b8c9",
+ name: "qbittorrent",
+ host: "local",
+ state: "running",
+ image: "lscr.io/linuxserver/qbittorrent:latest",
+ iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/qbittorrent.svg",
+ cpuUsage: 5.1,
+ memoryUsage: 380 * 1024 * 1024,
+ ports: [port(8080, 8080, "tcp")],
+ },
+ {
+ id: "e5f6a7b8c9d0",
+ name: "prowlarr",
+ host: "local",
+ state: "running",
+ image: "lscr.io/linuxserver/prowlarr:latest",
+ iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/prowlarr.svg",
+ cpuUsage: 0.8,
+ memoryUsage: 120 * 1024 * 1024,
+ ports: [port(9696, 9696, "tcp")],
+ },
+ {
+ id: "f6a7b8c9d0e1",
+ name: "overseerr",
+ host: "local",
+ state: "running",
+ image: "lscr.io/linuxserver/overseerr:latest",
+ iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/overseerr.svg",
+ cpuUsage: 1.2,
+ memoryUsage: 180 * 1024 * 1024,
+ ports: [port(5055, 5055, "tcp")],
+ },
+ {
+ id: "a7b8c9d0e1f2",
+ name: "homarr",
+ host: "local",
+ state: "running",
+ image: "ghcr.io/homarr-labs/homarr:latest",
+ iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/homarr.svg",
+ cpuUsage: 3.4,
+ memoryUsage: 290 * 1024 * 1024,
+ ports: [port(7575, 7575, "tcp")],
+ },
+ {
+ id: "b8c9d0e1f2a3",
+ name: "pihole",
+ host: "local",
+ state: "running",
+ image: "pihole/pihole:latest",
+ iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/pi-hole.svg",
+ cpuUsage: 0.5,
+ memoryUsage: 95 * 1024 * 1024,
+ ports: [port(80, 80, "tcp"), port(53, 53, "udp")],
+ },
+ {
+ id: "c9d0e1f2a3b4",
+ name: "nginx-proxy",
+ host: "local",
+ state: "running",
+ image: "jc21/nginx-proxy-manager:latest",
+ iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/nginx-proxy-manager.svg",
+ cpuUsage: 0.3,
+ memoryUsage: 65 * 1024 * 1024,
+ ports: [port(443, 443, "tcp"), port(81, 81, "tcp")],
+ },
+ {
+ id: "d0e1f2a3b4c5",
+ name: "watchtower",
+ host: "local",
+ state: "running",
+ image: "containrrr/watchtower:latest",
+ iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/watchtower.svg",
+ cpuUsage: 0.1,
+ memoryUsage: 30 * 1024 * 1024,
+ ports: [],
+ },
+ {
+ id: "e1f2a3b4c5d6",
+ name: "tdarr",
+ host: "local",
+ state: "exited",
+ image: "ghcr.io/haveagitgat/tdarr:latest",
+ iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/tdarr.svg",
+ cpuUsage: 0,
+ memoryUsage: 0,
+ ports: [],
+ },
+ {
+ id: "f2a3b4c5d6e7",
+ name: "bazarr",
+ host: "local",
+ state: "running",
+ image: "lscr.io/linuxserver/bazarr:latest",
+ iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/bazarr.svg",
+ cpuUsage: 0.6,
+ memoryUsage: 110 * 1024 * 1024,
+ ports: [port(6767, 6767, "tcp")],
+ },
+];
+
export const dockerContainersRequestHandler = createCachedWidgetRequestHandler({
queryKey: "dockerContainersResult",
widgetKind: "dockerContainers",
async requestAsync() {
+ if (isDemoMode) {
+ return mockContainers;
+ }
return await getContainersWithStatsAsync();
},
cacheDuration: dayjs.duration(20, "seconds"),
});
-const extractImage = (container: ContainerInfo) => container.Image.split("/").at(-1)?.split(":").at(0) ?? "";
+const extractImage = (container: ContainerInfo) => extractContainerImageName(container.Image);
+
+const findContainerByIdAsync = async (id: string) => {
+ const dockerInstances = DockerSingleton.getInstances();
+ const containers = await Promise.all(
+ dockerInstances.map(async ({ instance }) => {
+ const container = instance.getContainer(id);
+
+ return await new Promise((resolve) => {
+ container.inspect((err, data) => {
+ if (err || !data) {
+ resolve(null);
+ } else {
+ resolve(container);
+ }
+ });
+ });
+ }),
+ );
+
+ return containers.find((container) => container) ?? null;
+};
+
+export const getContainerLogsAsync = async (id: string, tail = 200) => {
+ const container = await findContainerByIdAsync(id);
+ if (!container) {
+ return null;
+ }
+
+ const rawLogs = await container.logs({
+ tail,
+ stdout: true,
+ stderr: true,
+ follow: false,
+ });
+
+ return decodeDockerLogs(rawLogs);
+};
+
+export const streamContainerLogsAsync = async (
+ id: string,
+ tail: number,
+ onData: (data: string) => void,
+ onError: (err: Error) => void,
+) => {
+ const container = await findContainerByIdAsync(id);
+ if (!container) {
+ onError(new Error("Container not found"));
+ return () => undefined;
+ }
+
+ const stream = (await container.logs({
+ tail,
+ stdout: true,
+ stderr: true,
+ follow: true,
+ })) as Readable;
+
+ const MAX_MESSAGE_SIZE = 1024 * 1024;
+ const processChunk = createDockerLogStreamProcessor(onData, onError, MAX_MESSAGE_SIZE);
+
+ const handleChunk = (chunk: Buffer) => {
+ const shouldContinue = processChunk(chunk);
+ if (!shouldContinue) {
+ stream.removeListener("data", handleChunk);
+ stream.removeListener("error", onError);
+ stream.destroy();
+ }
+ };
+
+ stream.on("data", handleChunk);
+ stream.on("error", onError);
+
+ return () => {
+ stream.removeListener("data", handleChunk);
+ stream.removeListener("error", onError);
+ stream.destroy();
+ };
+};
async function getContainersWithStatsAsync() {
const dockerInstances = DockerSingleton.getInstances();
@@ -117,8 +355,6 @@ export function calculateMemoryUsage(stats: ContainerStats): number {
return 0;
}
- // memory usage by default includes cache, which should not be shown as it is also not shown with docker stats command
- // See https://docs.docker.com/reference/cli/docker/container/stats/ how it is / was calculated
return (
stats.memory_stats.usage -
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
diff --git a/packages/request-handler/src/ups.ts b/packages/request-handler/src/ups.ts
new file mode 100644
index 000000000..acc97eb6d
--- /dev/null
+++ b/packages/request-handler/src/ups.ts
@@ -0,0 +1,20 @@
+import dayjs from "dayjs";
+
+import type { IntegrationKindByCategory } from "@homarr/definitions";
+import { createIntegrationAsync } from "@homarr/integrations";
+import type { UpsSummary } from "@homarr/integrations/types";
+
+import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler";
+
+export const upsSummariesRequestHandler = createCachedIntegrationRequestHandler<
+ UpsSummary[],
+ IntegrationKindByCategory<"ups">,
+ Record
+>({
+ async requestAsync(integration, _input) {
+ const integrationInstance = await createIntegrationAsync(integration);
+ return await integrationInstance.getUpsSummariesAsync();
+ },
+ cacheDuration: dayjs.duration(1, "minute"),
+ queryKey: "upsSummaries",
+});
diff --git a/packages/settings/index.ts b/packages/settings/index.ts
index 123d6a3ea..aded6d905 100644
--- a/packages/settings/index.ts
+++ b/packages/settings/index.ts
@@ -1 +1,2 @@
export * from "./src/context";
+export * from "./src/preferences/definitions";
diff --git a/packages/settings/src/preferences/definitions.ts b/packages/settings/src/preferences/definitions.ts
new file mode 100644
index 000000000..528ce8aba
--- /dev/null
+++ b/packages/settings/src/preferences/definitions.ts
@@ -0,0 +1,78 @@
+export const userPreferenceDefinitions = [
+ {
+ key: "colorScheme",
+ kind: "select",
+ guest: true,
+ mutationGroup: "colorScheme",
+ aliases: ["theme", "dark", "light", "auto"],
+ },
+ { key: "locale", kind: "select", guest: true, mutationGroup: "locale", aliases: ["language", "translation"] },
+ {
+ key: "defaultSearchEngineId",
+ kind: "searchEngine",
+ guest: false,
+ mutationGroup: "searchPreferences",
+ aliases: ["search", "search engine"],
+ },
+ {
+ key: "openSearchInNewTab",
+ kind: "boolean",
+ guest: false,
+ mutationGroup: "searchPreferences",
+ aliases: ["search", "new tab", "external"],
+ },
+ {
+ key: "ddgBangs",
+ kind: "boolean",
+ guest: false,
+ mutationGroup: "searchPreferences",
+ aliases: ["duckduckgo", "bangs", "search"],
+ },
+ {
+ key: "firstDayOfWeek",
+ kind: "select",
+ guest: false,
+ mutationGroup: "firstDayOfWeek",
+ aliases: ["calendar", "week", "weekday"],
+ },
+ {
+ key: "homeBoardId",
+ kind: "board",
+ guest: false,
+ mutationGroup: "homeBoards",
+ aliases: ["home", "board", "dashboard"],
+ },
+ {
+ key: "mobileHomeBoardId",
+ kind: "board",
+ guest: false,
+ mutationGroup: "homeBoards",
+ aliases: ["mobile", "home", "board", "dashboard"],
+ },
+ {
+ key: "pingIconsEnabled",
+ kind: "boolean",
+ guest: false,
+ mutationGroup: "pingIconsEnabled",
+ aliases: ["ping", "icon", "status"],
+ },
+ {
+ key: "fullPreferencesPage",
+ kind: "link",
+ guest: false,
+ mutationGroup: null,
+ aliases: ["settings", "manage", "profile"],
+ },
+] as const;
+
+export type UserPreferenceDefinition = (typeof userPreferenceDefinitions)[number];
+export type UserPreferenceKey = UserPreferenceDefinition["key"];
+export type UserPreferenceKind = UserPreferenceDefinition["kind"];
+export type UserPreferenceMutationGroup = Exclude;
+
+export const userPreferenceDefinitionByKey = Object.fromEntries(
+ userPreferenceDefinitions.map((definition) => [definition.key, definition]),
+) as Record;
+
+export const visiblePreferenceDefinitions = (isAuthenticated: boolean) =>
+ userPreferenceDefinitions.filter((definition) => definition.guest || isAuthenticated);
diff --git a/packages/spotlight/package.json b/packages/spotlight/package.json
index 39868e02e..b9c16aef8 100644
--- a/packages/spotlight/package.json
+++ b/packages/spotlight/package.json
@@ -29,6 +29,7 @@
"@homarr/integrations": "workspace:^0.1.0",
"@homarr/modals": "workspace:^0.1.0",
"@homarr/modals-collection": "workspace:^0.1.0",
+ "@homarr/notifications": "workspace:^0.1.0",
"@homarr/settings": "workspace:^0.1.0",
"@homarr/translation": "workspace:^0.1.0",
"@homarr/ui": "workspace:^0.1.0",
diff --git a/packages/spotlight/src/components/actions/items/children-action-item.tsx b/packages/spotlight/src/components/actions/items/children-action-item.tsx
index f7c98c519..3d5427506 100644
--- a/packages/spotlight/src/components/actions/items/children-action-item.tsx
+++ b/packages/spotlight/src/components/actions/items/children-action-item.tsx
@@ -33,7 +33,10 @@ export const ChildrenActionItem = ({ childrenOptions, action, query, setChildren
diff --git a/packages/spotlight/src/components/actions/items/group-action-item.tsx b/packages/spotlight/src/components/actions/items/group-action-item.tsx
index eb48e04b2..e6a802c26 100644
--- a/packages/spotlight/src/components/actions/items/group-action-item.tsx
+++ b/packages/spotlight/src/components/actions/items/group-action-item.tsx
@@ -44,6 +44,9 @@ export const SpotlightGroupActionItem =
setQuery(interaction.query);
setTimeout(() => selectAction(0, spotlightStore));
} else if (interaction.type === "mode") {
+ if (interaction.query !== undefined) {
+ setQuery(interaction.query);
+ }
setMode(interaction.mode);
} else if (interaction.type === "children") {
setChildrenOptions(interaction);
@@ -58,7 +61,8 @@ export const SpotlightGroupActionItem =
interaction.type !== "mode" &&
interaction.type !== "children" &&
interaction.type !== "none" &&
- interaction.type !== "setQuery"
+ interaction.type !== "setQuery" &&
+ (interaction.type !== "javaScript" || interaction.closeSpotlightOnTrigger !== false)
}
className={classes.spotlightAction}
>
diff --git a/packages/spotlight/src/components/spotlight.tsx b/packages/spotlight/src/components/spotlight.tsx
index 4ca086f3b..89b1b0d63 100644
--- a/packages/spotlight/src/components/spotlight.tsx
+++ b/packages/spotlight/src/components/spotlight.tsx
@@ -1,24 +1,33 @@
"use client";
import type { Dispatch, SetStateAction } from "react";
-import { useMemo, useRef, useState } from "react";
+import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { ActionIcon, Center, Group, Kbd } from "@mantine/core";
import { Spotlight as MantineSpotlight } from "@mantine/spotlight";
-import { IconSearch, IconX } from "@tabler/icons-react";
+import { IconArrowLeft, IconSearch, IconX } from "@tabler/icons-react";
+import { useSetAtom } from "jotai";
import { hotkeys } from "@homarr/definitions";
import type { TranslationObject } from "@homarr/translation";
import { useI18n } from "@homarr/translation/client";
+import type { OpenMediaRequestSearchOptions } from "../index";
import type { inferSearchInteractionOptions } from "../lib/interaction";
import type { SearchMode } from "../lib/mode";
import { searchModes } from "../modes";
-import { useHomeEmptyGroups } from "../modes/help/home-empty-groups";
-import { selectAction, spotlightStore } from "../spotlight-store";
+import { useHomeEmptyGroupsWithPreferences } from "../modes/help/home-empty-groups";
+import {
+ mediaRequestSearchEvent,
+ mediaRequestSearchScopeAtom,
+ selectAction,
+ spotlightActions,
+ spotlightStore,
+} from "../spotlight-store";
import { SpotlightChildrenActions } from "./actions/children-actions";
import { SpotlightActionGroups } from "./actions/groups/action-group";
type SearchModeKey = keyof TranslationObject["search"]["mode"];
+type ChildrenOptions = inferSearchInteractionOptions<"children">;
const defaultMode = "home";
export const Spotlight = () => {
@@ -31,7 +40,6 @@ export const Spotlight = () => {
return null;
}
- // We use the "key" below to prevent the 'Different amounts of hooks' error
return (
);
@@ -46,13 +54,57 @@ interface SpotlightWithActiveModeProps {
const SpotlightWithActiveMode = ({ modeState, queryState, activeMode }: SpotlightWithActiveModeProps) => {
const [query, setQuery] = queryState;
const [mode, setMode] = modeState;
- const [childrenOptions, setChildrenOptions] = useState | null>(null);
+ const [childrenStack, setChildrenStack] = useState([]);
+ const childrenOptions = childrenStack.at(-1) ?? null;
const t = useI18n();
const inputRef = useRef(null);
- // Works as always the same amount of hooks are executed
+ const setMediaRequestSearchScope = useSetAtom(mediaRequestSearchScopeAtom);
const useGroups = "groups" in activeMode ? () => activeMode.groups : activeMode.useGroups;
const groups = useGroups();
- const homeEmptyGroups = useHomeEmptyGroups();
+ const homeEmptyGroups = useHomeEmptyGroupsWithPreferences();
+ const hasModeCharacter = activeMode.modeKey !== defaultMode && activeMode.character !== undefined;
+
+ const clearChildrenStack = useCallback(() => {
+ setChildrenStack([]);
+ }, []);
+
+ const pushChildrenOptions = useCallback(
+ (options: ChildrenOptions) => {
+ setChildrenStack((currentStack) => [...currentStack, options]);
+ setQuery("");
+ setTimeout(() => selectAction(0, spotlightStore));
+ },
+ [setQuery],
+ );
+
+ const popChildrenOptions = useCallback(() => {
+ setChildrenStack((currentStack) => currentStack.slice(0, -1));
+ setQuery("");
+ setTimeout(() => selectAction(0, spotlightStore));
+ }, [setQuery]);
+
+ useEffect(() => {
+ const handleMediaRequestSearch = (event: Event) => {
+ const { integrationIds, query } = (event as CustomEvent).detail ?? {};
+ setMediaRequestSearchScope({ integrationIds });
+ setMode("media");
+ clearChildrenStack();
+ setQuery(query ?? "");
+ spotlightActions.open();
+ setTimeout(() => selectAction(0, spotlightStore));
+ };
+
+ window.addEventListener(mediaRequestSearchEvent, handleMediaRequestSearch);
+ return () => {
+ window.removeEventListener(mediaRequestSearchEvent, handleMediaRequestSearch);
+ };
+ }, [setMediaRequestSearchScope, setMode, setQuery, clearChildrenStack]);
+
+ useEffect(() => {
+ if (mode !== "media") {
+ setMediaRequestSearchScope({});
+ }
+ }, [mode, setMediaRequestSearchScope]);
return (
{
setMode(defaultMode);
- setChildrenOptions(null);
+ clearChildrenStack();
setQuery("");
}}
query={query}
@@ -69,17 +121,14 @@ const SpotlightWithActiveMode = ({ modeState, queryState, activeMode }: Spotligh
const sanitizedQuery = mode === "external" && nextQuery.startsWith("!") ? nextQuery.slice(1) : nextQuery;
setQuery(sanitizedQuery);
- // Only switch modes when a single trigger character is typed
if (sanitizedQuery.length !== 1) return;
- const modeToActivate = searchModes.find((mode) => mode.character === sanitizedQuery);
+ const modeToActivate = searchModes.find((searchMode) => searchMode.character === sanitizedQuery);
if (!modeToActivate) return;
setMode(modeToActivate.modeKey);
- setChildrenOptions(null);
-
+ clearChildrenStack();
setQuery("");
-
setTimeout(() => selectAction(0, spotlightStore));
}}
store={spotlightStore}
@@ -87,13 +136,13 @@ const SpotlightWithActiveMode = ({ modeState, queryState, activeMode }: Spotligh
- {activeMode.modeKey !== defaultMode ? {activeMode.character} : null}
+ {hasModeCharacter ? {activeMode.character} : null}
}
styles={{
@@ -102,31 +151,53 @@ const SpotlightWithActiveMode = ({ modeState, queryState, activeMode }: Spotligh
},
}}
rightSection={
- mode === defaultMode ? null : (
+ mode === defaultMode && childrenStack.length === 0 ? null : (
0
+ ? t("search.mode.command.group.preferences.children.detail.backAction")
+ : t("common.action.close")
+ }
onClick={() => {
+ if (childrenStack.length > 0) {
+ popChildrenOptions();
+ inputRef.current?.focus();
+ return;
+ }
+
setMode(defaultMode);
- setChildrenOptions(null);
+ clearChildrenStack();
inputRef.current?.focus();
}}
variant="subtle"
- aria-label={t("common.action.close")}
+ aria-label={
+ childrenStack.length > 0
+ ? t("search.mode.command.group.preferences.children.detail.backAction")
+ : t("common.action.close")
+ }
>
-
+ {childrenStack.length > 0 ? : }
)
}
value={query}
onKeyDown={(event) => {
- if (query.length === 0 && mode !== defaultMode && event.key === "Backspace") {
+ if (query.length !== 0 || event.key !== "Backspace") return;
+
+ if (childrenStack.length > 0) {
+ popChildrenOptions();
+ return;
+ }
+
+ if (mode !== defaultMode) {
setMode(defaultMode);
- setChildrenOptions(null);
+ clearChildrenStack();
}
}}
/>
{childrenOptions ? (
-
+
) : null}
@@ -134,25 +205,21 @@ const SpotlightWithActiveMode = ({ modeState, queryState, activeMode }: Spotligh
{childrenOptions ? (
) : (
{
- setMode(mode);
- setChildrenOptions(null);
+ setMode={(nextMode) => {
+ setMode(nextMode);
+ clearChildrenStack();
setTimeout(() => selectAction(0, spotlightStore));
}}
setChildrenOptions={(options) => {
- setChildrenOptions(options);
-
- setTimeout(() => {
- setQuery("");
- selectAction(0, spotlightStore);
- });
+ pushChildrenOptions(options);
}}
query={query}
groups={mode === defaultMode && query.length === 0 ? [...homeEmptyGroups] : groups}
diff --git a/packages/spotlight/src/index.ts b/packages/spotlight/src/index.ts
index 18298a57b..3433b7125 100644
--- a/packages/spotlight/src/index.ts
+++ b/packages/spotlight/src/index.ts
@@ -1,9 +1,9 @@
"use client";
-import { spotlightActions } from "./spotlight-store";
+import { mediaRequestSearchEvent, spotlightActions } from "./spotlight-store";
export { Spotlight } from "./components/spotlight";
-export { openSpotlight };
+export { openSpotlight, openMediaRequestSearch };
export {
SpotlightProvider,
useRegisterSpotlightContextResults,
@@ -11,3 +11,18 @@ export {
} from "./modes/home/context";
const openSpotlight = spotlightActions.open;
+
+export interface OpenMediaRequestSearchOptions {
+ integrationIds?: string[];
+ query?: string;
+}
+
+const openMediaRequestSearch = (options: OpenMediaRequestSearchOptions = {}) => {
+ if (typeof window !== "undefined") {
+ window.dispatchEvent(new CustomEvent(mediaRequestSearchEvent, { detail: options }));
+ }
+
+ spotlightActions.open();
+};
+
+export { mediaRequestSearchEvent };
diff --git a/packages/spotlight/src/lib/interaction.ts b/packages/spotlight/src/lib/interaction.ts
index 060a35c51..c4b8ca212 100644
--- a/packages/spotlight/src/lib/interaction.ts
+++ b/packages/spotlight/src/lib/interaction.ts
@@ -10,9 +10,12 @@ const createSearchInteraction = (type: TType) => ({
// This is used to define search interactions with their options
const searchInteractions = [
createSearchInteraction("link").optionsType<{ href: string; newTab?: boolean }>(),
- createSearchInteraction("javaScript").optionsType<{ onSelect: () => MaybePromise }>(),
+ createSearchInteraction("javaScript").optionsType<{
+ onSelect: () => MaybePromise;
+ closeSpotlightOnTrigger?: boolean;
+ }>(),
createSearchInteraction("setQuery").optionsType<{ query: string }>(),
- createSearchInteraction("mode").optionsType<{ mode: keyof TranslationObject["search"]["mode"] }>(),
+ createSearchInteraction("mode").optionsType<{ mode: keyof TranslationObject["search"]["mode"]; query?: string }>(),
createSearchInteraction("children").optionsType<{
// eslint-disable-next-line @typescript-eslint/no-explicit-any
useActions: CreateChildrenOptionsProps["useActions"];
diff --git a/packages/spotlight/src/modes/command/children/language.tsx b/packages/spotlight/src/modes/command/children/language.tsx
index 6178872b8..e73f91d84 100644
--- a/packages/spotlight/src/modes/command/children/language.tsx
+++ b/packages/spotlight/src/modes/command/children/language.tsx
@@ -1,16 +1,17 @@
-import { Group, Stack, Text } from "@mantine/core";
-import { IconCheck } from "@tabler/icons-react";
+import { Group, Text } from "@mantine/core";
import { localeConfigurations, supportedLanguages } from "@homarr/translation";
-import { useChangeLocale, useCurrentLocale, useI18n } from "@homarr/translation/client";
+import { useUserPreference } from "../../../preferences/use-user-preference";
import { LanguageIcon } from "@homarr/ui";
import { createChildrenOptions } from "../../../lib/children";
+import { createCheckmarkPreferenceAction, PreferenceDetailHeader } from "./preferences/action-row";
export const languageChildrenOptions = createChildrenOptions>({
useActions: (_, query) => {
const normalizedQuery = query.trim().toLowerCase();
- const currentLocale = useCurrentLocale();
+ const { value: currentLocale, setValue: setLocale, isPending } = useUserPreference("locale");
+
return supportedLanguages
.map((localeKey) => ({ localeKey, configuration: localeConfigurations[localeKey] }))
.filter(
@@ -18,7 +19,7 @@ export const languageChildrenOptions = createChildrenOptions
Math.min(
languageA.configuration.name.toLowerCase().indexOf(normalizedQuery),
@@ -29,38 +30,25 @@ export const languageChildrenOptions = createChildrenOptions ({
- key: localeKey,
- Component() {
- return (
-
-
-
-
- {configuration.name}
-
- ({configuration.translatedName})
-
-
+ .map(({ localeKey, configuration }) =>
+ createCheckmarkPreferenceAction({
+ key: localeKey,
+ isSelected: localeKey === currentLocale,
+ onSelect: () => setLocale(localeKey as never),
+ isPending,
+ labelContent: (
+
+
+
+ {configuration.name}
+
+ ({configuration.translatedName})
+
- {localeKey === currentLocale && }
- );
- },
- useInteraction() {
- const { changeLocale } = useChangeLocale();
-
- return { type: "javaScript", onSelect: () => changeLocale(localeKey) };
- },
- }));
- },
- DetailComponent: () => {
- const t = useI18n();
-
- return (
-
- {t("search.mode.command.group.globalCommand.option.language.children.detail.title")}
-
- );
+ ),
+ }),
+ );
},
+ DetailComponent: () => ,
});
diff --git a/packages/spotlight/src/modes/command/children/preferences/action-row.tsx b/packages/spotlight/src/modes/command/children/preferences/action-row.tsx
new file mode 100644
index 000000000..1e309a388
--- /dev/null
+++ b/packages/spotlight/src/modes/command/children/preferences/action-row.tsx
@@ -0,0 +1,68 @@
+import type { ReactNode } from "react";
+import { Center, Group, Loader, Stack, Text } from "@mantine/core";
+import { IconCheck } from "@tabler/icons-react";
+
+import { useScopedI18n } from "@homarr/translation/client";
+
+import { interaction } from "../../../../lib/interaction";
+
+export const PreferenceDetailHeader = ({ titleKey }: { titleKey: string }) => {
+ const tOption = useScopedI18n("search.mode.command.group.preferences.option");
+ const t = useScopedI18n("search.mode.command.group.preferences");
+
+ return (
+
+ {tOption(titleKey as never)}
+
+ {t("children.detail.backHint")}
+
+
+ );
+};
+
+export const createCheckmarkPreferenceAction = ({
+ key,
+ label,
+ labelContent,
+ isSelected,
+ onSelect,
+ dimmed,
+ isPending,
+}: {
+ key: string;
+ label?: string;
+ labelContent?: ReactNode;
+ isSelected: boolean;
+ onSelect: () => void;
+ dimmed?: boolean;
+ isPending?: boolean;
+}) => ({
+ key,
+ Component() {
+ return (
+
+ {labelContent ?? {label}}
+ {isSelected && }
+
+ );
+ },
+ useInteraction: interaction.javaScript(() => ({
+ onSelect: isPending ? () => undefined : onSelect,
+ closeSpotlightOnTrigger: false,
+ })),
+});
+
+export const createLoadingPreferenceAction = () => ({
+ key: "loading",
+ Component() {
+ return (
+
+
+
+ );
+ },
+ useInteraction: interaction.javaScript(() => ({
+ onSelect: () => undefined,
+ closeSpotlightOnTrigger: false,
+ })),
+});
diff --git a/packages/spotlight/src/modes/command/children/preferences/index.tsx b/packages/spotlight/src/modes/command/children/preferences/index.tsx
new file mode 100644
index 000000000..8eafebb6c
--- /dev/null
+++ b/packages/spotlight/src/modes/command/children/preferences/index.tsx
@@ -0,0 +1 @@
+export { settingsChildrenOptions } from "./settings-children";
diff --git a/packages/spotlight/src/modes/command/children/preferences/selectable-actions.tsx b/packages/spotlight/src/modes/command/children/preferences/selectable-actions.tsx
new file mode 100644
index 000000000..51e7e9cd8
--- /dev/null
+++ b/packages/spotlight/src/modes/command/children/preferences/selectable-actions.tsx
@@ -0,0 +1,55 @@
+import { createCheckmarkPreferenceAction } from "./action-row";
+
+interface SelectablePreferenceItem {
+ key: string;
+ label: string;
+ value: string;
+}
+
+export const createSelectablePreferenceActions = ({
+ query,
+ currentValue,
+ noneLabel,
+ unavailableLabel,
+ items,
+ onSelect,
+ isPending,
+}: {
+ query: string;
+ currentValue: string | null;
+ noneLabel: string;
+ unavailableLabel: string;
+ items: SelectablePreferenceItem[];
+ onSelect: (value: string | null) => void;
+ isPending?: boolean;
+}) => {
+ const hasCurrentSelection = currentValue !== null && !items.some((item) => item.value === currentValue);
+ const orphanedItem: SelectablePreferenceItem | null = hasCurrentSelection
+ ? { key: `orphaned-${currentValue}`, label: unavailableLabel, value: currentValue }
+ : null;
+ const normalizedQuery = query.trim().toLowerCase();
+ const filteredItems = items.filter((item) => item.label.toLowerCase().includes(normalizedQuery));
+ const visibleItems = orphanedItem ? [orphanedItem, ...filteredItems] : filteredItems;
+
+ const noneAction = createCheckmarkPreferenceAction({
+ key: "none",
+ label: noneLabel,
+ isSelected: currentValue === null,
+ onSelect: () => onSelect(null),
+ dimmed: true,
+ isPending,
+ });
+
+ const itemActions = visibleItems.map((item) =>
+ createCheckmarkPreferenceAction({
+ key: item.key,
+ label: item.label,
+ isSelected: currentValue === item.value,
+ onSelect: () => onSelect(item.value),
+ dimmed: item.key.startsWith("orphaned-"),
+ isPending,
+ }),
+ );
+
+ return [noneAction, ...itemActions];
+};
diff --git a/packages/spotlight/src/modes/command/children/preferences/settings-children.tsx b/packages/spotlight/src/modes/command/children/preferences/settings-children.tsx
new file mode 100644
index 000000000..fc0760583
--- /dev/null
+++ b/packages/spotlight/src/modes/command/children/preferences/settings-children.tsx
@@ -0,0 +1,156 @@
+"use client";
+
+import { Group, Stack, Switch, Text } from "@mantine/core";
+import type { DayOfWeek } from "@mantine/dates";
+import dayjs from "dayjs";
+import localeData from "dayjs/plugin/localeData";
+
+import { clientApi } from "@homarr/api/client";
+import { useSession } from "@homarr/auth/client";
+import type { ColorScheme } from "@homarr/definitions";
+import { visiblePreferenceDefinitions } from "@homarr/settings";
+import { localeConfigurations } from "@homarr/translation";
+import { useScopedI18n } from "@homarr/translation/client";
+import type { TablerIcon } from "@homarr/ui";
+
+import { createChildrenOptions } from "../../../../lib/children";
+import type { ChildrenAction } from "../../../../lib/children";
+import { interaction } from "../../../../lib/interaction";
+import { useUserPreferences } from "../../../../preferences/use-user-preference";
+import { preferenceChildrenOptionsByKey, preferenceIcons } from "../../../../preferences/preference-registry";
+
+dayjs.extend(localeData);
+
+type SettingsAction = ChildrenAction>;
+
+const BooleanRow = ({
+ label,
+ Icon,
+ checked,
+ isPending,
+}: {
+ label: string;
+ Icon: TablerIcon;
+ checked: boolean;
+ isPending: boolean;
+}) => (
+
+
+
+ {label}
+
+
+
+);
+
+const SelectRow = ({ label, Icon, valueLabel }: { label: string; Icon: TablerIcon; valueLabel: string }) => (
+
+
+
+ {label}
+
+
+ {valueLabel}
+
+
+);
+
+const LinkRow = ({ label, Icon }: { label: string; Icon: TablerIcon }) => (
+
+
+ {label}
+
+);
+
+const SELECT_KINDS = new Set(["select", "searchEngine", "board"]);
+
+const useSettingsActions = (_: Record, query: string): SettingsAction[] => {
+ const t = useScopedI18n("search.mode.command.group.preferences.option");
+ const tColorScheme = useScopedI18n("common.colorScheme.options");
+ const { data: session } = useSession();
+ const isAuthenticated = Boolean(session?.user);
+ const normalizedQuery = query.trim().toLowerCase();
+ const preferences = useUserPreferences();
+
+ const boardsQuery = clientApi.board.getAllBoards.useQuery(undefined, { enabled: isAuthenticated });
+ const searchEnginesQuery = clientApi.searchEngine.getSelectable.useQuery(undefined, { enabled: isAuthenticated });
+
+ const valueLabelResolvers: Record string> = {
+ colorScheme: () => tColorScheme(preferences.getPreference("colorScheme").value as ColorScheme),
+ locale: () => {
+ const val = preferences.getPreference("locale").value;
+ return localeConfigurations[val as keyof typeof localeConfigurations]?.name ?? String(val);
+ },
+ defaultSearchEngineId: () =>
+ searchEnginesQuery.data?.find((e) => e.value === preferences.getPreference("defaultSearchEngineId").value)
+ ?.label ?? t("searchEngine.none" as never),
+ firstDayOfWeek: () => dayjs.weekdays(false)[preferences.getPreference("firstDayOfWeek").value as DayOfWeek] ?? "",
+ homeBoardId: () =>
+ boardsQuery.data?.find((b) => b.id === preferences.getPreference("homeBoardId").value)?.name ??
+ t("board.none" as never),
+ mobileHomeBoardId: () =>
+ boardsQuery.data?.find((b) => b.id === preferences.getPreference("mobileHomeBoardId").value)?.name ??
+ t("board.none" as never),
+ };
+
+ return visiblePreferenceDefinitions(isAuthenticated)
+ .map((definition): SettingsAction | null => {
+ const label = t(`${definition.key}.label` as never);
+ const Icon = preferenceIcons[definition.key];
+
+ const matchesSearch = [label, ...definition.aliases].some((v) => v.toLowerCase().includes(normalizedQuery));
+ if (!matchesSearch) return null;
+
+ if (definition.kind === "boolean") {
+ const pref = preferences.getPreference(definition.key);
+ return {
+ key: definition.key,
+ Component: () => (
+
+ ),
+ useInteraction: interaction.javaScript(() => ({
+ onSelect: pref.isPending ? () => undefined : () => pref.setValue(!(pref.value as boolean) as never),
+ closeSpotlightOnTrigger: false,
+ })),
+ };
+ }
+
+ if (definition.kind === "link") {
+ return {
+ key: definition.key,
+ Component: () => ,
+ useInteraction: interaction.link(() => ({ href: `/manage/users/${session?.user.id ?? ""}/general` })),
+ };
+ }
+
+ if (SELECT_KINDS.has(definition.kind)) {
+ const children = preferenceChildrenOptionsByKey[definition.key];
+ if (!children) return null;
+ return {
+ key: definition.key,
+ Component: () => (
+
+ ),
+ useInteraction: interaction.children(children),
+ };
+ }
+
+ return null;
+ })
+ .filter((action): action is SettingsAction => action !== null);
+};
+
+export const settingsChildrenOptions = createChildrenOptions>({
+ useActions: useSettingsActions,
+ DetailComponent: () => {
+ const t = useScopedI18n("search.mode.command.group.preferences");
+ return (
+
+ {t("title")}
+
+ {t("children.detail.backHint")}
+
+
+ );
+ },
+});
diff --git a/packages/spotlight/src/modes/command/global-group.tsx b/packages/spotlight/src/modes/command/global-group.tsx
index 4d2f60d37..f1d44e434 100644
--- a/packages/spotlight/src/modes/command/global-group.tsx
+++ b/packages/spotlight/src/modes/command/global-group.tsx
@@ -1,14 +1,10 @@
-import { Group, Text, useComputedColorScheme, useMantineColorScheme } from "@mantine/core";
+import { Group, Text } from "@mantine/core";
import {
IconBox,
IconCategoryPlus,
- IconDeviceDesktop,
IconFileImport,
- IconLanguage,
IconMailForward,
- IconMoon,
IconPlug,
- IconSun,
IconUserPlus,
IconUsersGroup,
} from "@tabler/icons-react";
@@ -22,7 +18,6 @@ import type { TablerIcon } from "@homarr/ui";
import { createGroup } from "../../lib/group";
import type { inferSearchInteractionDefinition, SearchInteraction } from "../../lib/interaction";
import { interaction } from "../../lib/interaction";
-import { languageChildrenOptions } from "./children/language";
import { newIntegrationChildrenOptions } from "./children/new-integration";
// This has to be type so it can be interpreted as Record.
@@ -52,33 +47,9 @@ export const globalCommandGroup = createGroup({
},
useOptions() {
const tOption = useScopedI18n("search.mode.command.group.globalCommand.option");
- const { colorScheme } = useMantineColorScheme();
- const computedColorScheme = useComputedColorScheme("light");
const { data: session } = useSession();
const commands: (Command & { hidden?: boolean })[] = [
- {
- commandKey: "colorScheme",
- icon: colorScheme === "auto" ? IconDeviceDesktop : computedColorScheme === "dark" ? IconSun : IconMoon,
- name:
- colorScheme === "auto"
- ? tOption("colorScheme.auto")
- : tOption(`colorScheme.${computedColorScheme === "dark" ? "light" : "dark"}`),
- useInteraction: () => {
- const { toggleColorScheme } = useMantineColorScheme();
-
- return {
- type: "javaScript",
- onSelect: toggleColorScheme,
- };
- },
- },
- {
- commandKey: "language",
- icon: IconLanguage,
- name: tOption("language.label"),
- useInteraction: interaction.children(languageChildrenOptions),
- },
{
commandKey: "newBoard",
icon: IconCategoryPlus,
diff --git a/packages/spotlight/src/modes/command/index.tsx b/packages/spotlight/src/modes/command/index.tsx
index eee549dd8..30d8db633 100644
--- a/packages/spotlight/src/modes/command/index.tsx
+++ b/packages/spotlight/src/modes/command/index.tsx
@@ -1,9 +1,10 @@
import type { SearchMode } from "../../lib/mode";
import { contextSpecificActionsSearchGroups } from "./context-specific-group";
import { globalCommandGroup } from "./global-group";
+import { preferencesGroup } from "../preferences/groups";
export const commandMode = {
modeKey: "command",
character: ">",
- groups: [contextSpecificActionsSearchGroups, globalCommandGroup],
+ groups: [contextSpecificActionsSearchGroups, preferencesGroup, globalCommandGroup],
} satisfies SearchMode;
diff --git a/packages/spotlight/src/modes/external/search-engines-search-group.tsx b/packages/spotlight/src/modes/external/search-engines-search-group.tsx
index da965725b..34890b159 100644
--- a/packages/spotlight/src/modes/external/search-engines-search-group.tsx
+++ b/packages/spotlight/src/modes/external/search-engines-search-group.tsx
@@ -64,6 +64,36 @@ type MediaRequestChildrenProps = {
kind: IntegrationKind;
url: string;
id: string;
+ permissions?: {
+ hasInteractAccess: boolean;
+ };
+ };
+};
+
+export const useMediaRequestSearchInteraction = (
+ integration: MediaRequestChildrenProps["integration"],
+ searchResult: FromIntegrationSearchResult,
+): inferSearchInteractionDefinition<"link" | "children"> => {
+ const { openSearchInNewTab } = useSettings();
+
+ const type = searchResult.type;
+ if (type === "person") {
+ return {
+ type: "link",
+ href: searchResult.link,
+ newTab: openSearchInNewTab,
+ };
+ }
+
+ return {
+ type: "children",
+ ...mediaRequestsChildrenOptions({
+ result: {
+ ...searchResult,
+ type,
+ },
+ integration,
+ }),
};
};
@@ -71,8 +101,6 @@ export const useFromIntegrationSearchInteraction = (
searchEngine: SearchEngine,
searchResult: FromIntegrationSearchResult,
): inferSearchInteractionDefinition<"link" | "javaScript" | "children"> => {
- const { openSearchInNewTab } = useSettings();
-
if (searchEngine.type !== "fromIntegration") {
throw new Error("Invalid search engine type");
}
@@ -87,25 +115,7 @@ export const useFromIntegrationSearchInteraction = (
) &&
"type" in searchResult
) {
- const type = searchResult.type;
- if (type === "person") {
- return {
- type: "link",
- href: searchResult.link,
- newTab: openSearchInNewTab,
- };
- }
-
- return {
- type: "children",
- ...mediaRequestsChildrenOptions({
- result: {
- ...searchResult,
- type,
- },
- integration: searchEngine.integration,
- }),
- };
+ return useMediaRequestSearchInteraction(searchEngine.integration, searchResult);
}
return {
@@ -115,13 +125,15 @@ export const useFromIntegrationSearchInteraction = (
};
};
-const mediaRequestsChildrenOptions = createChildrenOptions({
+export const mediaRequestsChildrenOptions = createChildrenOptions({
useActions() {
const { openModal } = useModalAction(RequestMediaModal);
return [
{
key: "request",
- hide: (option) => option.result.inLibrary,
+ hide: (option) =>
+ (option.result.type === "movie" && option.result.inLibrary) ||
+ option.integration.permissions?.hasInteractAccess === false,
Component(option) {
const t = useScopedI18n("search.mode.media");
return (
diff --git a/packages/spotlight/src/modes/help/home-empty-groups.tsx b/packages/spotlight/src/modes/help/home-empty-groups.tsx
index e035e66b9..459e69df0 100644
--- a/packages/spotlight/src/modes/help/home-empty-groups.tsx
+++ b/packages/spotlight/src/modes/help/home-empty-groups.tsx
@@ -19,12 +19,13 @@ import type { SearchMode } from "../../lib/mode";
import { appIntegrationBoardMode } from "../app-integration-board";
import { commandMode } from "../command";
import { externalMode } from "../external";
+import { mediaMode } from "../media";
import { pageMode } from "../page";
+import { preferencesGroup } from "../preferences/groups";
import { userGroupMode } from "../user-group";
-// We need type to allow it as generic type for Record
-// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
type QuickLinkOption = {
+ key: string;
icon: TablerIcon;
name: string;
path: string;
@@ -33,7 +34,7 @@ type QuickLinkOption = {
export const useHomeEmptyGroups = () => {
const { data: session } = useSession();
const tPages = useScopedI18n("search.mode.page.group.page.option");
- const visibleSearchModes: SearchMode[] = [appIntegrationBoardMode, externalMode, commandMode, pageMode];
+ const visibleSearchModes: SearchMode[] = [appIntegrationBoardMode, externalMode, mediaMode, commandMode, pageMode];
if (session?.user.permissions.includes("admin")) {
visibleSearchModes.unshift(userGroupMode);
@@ -41,19 +42,13 @@ export const useHomeEmptyGroups = () => {
return [
createGroup({
- keyPath: "path",
+ keyPath: "key",
title: () => "Quick links",
options: (() => {
const quickLinks: QuickLinkOption[] = [];
- if (session?.user) {
- quickLinks.push({
- icon: IconSettings,
- path: `/manage/users/${session.user.id}/general`,
- name: tPages("preferences.label"),
- });
- }
quickLinks.push({
+ key: "manageBoards",
icon: IconLayoutDashboard,
path: "/manage/boards",
name: tPages("manageBoard.label"),
@@ -61,6 +56,7 @@ export const useHomeEmptyGroups = () => {
if (session?.user.permissions.includes("admin")) {
quickLinks.push({
+ key: "manageSettings",
icon: IconSettings,
path: "/manage/settings",
name: tPages("manageSettings.label"),
@@ -69,6 +65,7 @@ export const useHomeEmptyGroups = () => {
if (session) {
quickLinks.push({
+ key: "manageSearchEngines",
icon: IconSearch,
path: "/manage/search-engines",
name: tPages("manageSearchEngine.label"),
@@ -87,7 +84,7 @@ export const useHomeEmptyGroups = () => {
useInteraction: interaction.link(({ path }) => ({ href: path })),
}),
createGroup({
- keyPath: "character",
+ keyPath: "modeKey",
title: (t) => t("search.mode.help.group.mode.title"),
options: visibleSearchModes.map(({ character, modeKey }) => ({ character, modeKey })),
Component: ({ modeKey, character }) => {
@@ -96,7 +93,7 @@ export const useHomeEmptyGroups = () => {
return (
{t("help")}
- {character}
+ {character && {character}}
);
},
@@ -138,3 +135,5 @@ export const useHomeEmptyGroups = () => {
}),
] as const;
};
+
+export const useHomeEmptyGroupsWithPreferences = () => [preferencesGroup, ...useHomeEmptyGroups()];
diff --git a/packages/spotlight/src/modes/home/home-search-engine-group.tsx b/packages/spotlight/src/modes/home/home-search-engine-group.tsx
index 40e40f131..799d2216d 100644
--- a/packages/spotlight/src/modes/home/home-search-engine-group.tsx
+++ b/packages/spotlight/src/modes/home/home-search-engine-group.tsx
@@ -20,6 +20,7 @@ type GroupItem = {
name: string;
description?: string;
icon: TablerIcon | string;
+ disabled?: boolean;
useInteraction: (query: string) => inferSearchInteractionDefinition;
};
@@ -37,7 +38,7 @@ export const homeSearchEngineGroup = createGroup({
);
return (
-
+
{icon}
{item.name}
@@ -91,9 +92,7 @@ const createDefaultSearchEntries = (
query: string,
t: TranslationFunction,
): GroupItem[] => {
- if (!session?.user && !defaultSearchEngine) {
- return [];
- }
+ if (!session?.user && !defaultSearchEngine) return [];
if (!defaultSearchEngine) {
return [
diff --git a/packages/spotlight/src/modes/home/index.tsx b/packages/spotlight/src/modes/home/index.tsx
index 7851e6a39..67ef6db2a 100644
--- a/packages/spotlight/src/modes/home/index.tsx
+++ b/packages/spotlight/src/modes/home/index.tsx
@@ -1,9 +1,10 @@
import type { SearchMode } from "../../lib/mode";
+import { preferencesGroup } from "../preferences/groups";
import { contextSpecificSearchGroups } from "./context-specific-group";
import { homeSearchEngineGroup } from "./home-search-engine-group";
export const homeMode = {
character: undefined,
modeKey: "home",
- groups: [homeSearchEngineGroup, contextSpecificSearchGroups],
+ groups: [preferencesGroup, homeSearchEngineGroup, contextSpecificSearchGroups],
} satisfies SearchMode;
diff --git a/packages/spotlight/src/modes/index.tsx b/packages/spotlight/src/modes/index.tsx
index fa6e81916..0c860069f 100644
--- a/packages/spotlight/src/modes/index.tsx
+++ b/packages/spotlight/src/modes/index.tsx
@@ -2,6 +2,7 @@ import { appIntegrationBoardMode } from "./app-integration-board";
import { commandMode } from "./command";
import { externalMode } from "./external";
import { homeMode } from "./home";
+import { mediaMode } from "./media";
import { pageMode } from "./page";
import { userGroupMode } from "./user-group";
@@ -11,5 +12,6 @@ export const searchModes = [
externalMode,
commandMode,
pageMode,
+ mediaMode,
homeMode,
] as const;
diff --git a/packages/spotlight/src/modes/media/index.tsx b/packages/spotlight/src/modes/media/index.tsx
new file mode 100644
index 000000000..46b7a661f
--- /dev/null
+++ b/packages/spotlight/src/modes/media/index.tsx
@@ -0,0 +1,8 @@
+import type { SearchMode } from "../../lib/mode";
+import { mediaRequestSearchGroup } from "./media-request-search-group";
+
+export const mediaMode = {
+ modeKey: "media",
+ character: undefined,
+ groups: [mediaRequestSearchGroup],
+} satisfies SearchMode;
diff --git a/packages/spotlight/src/modes/media/media-request-search-group.tsx b/packages/spotlight/src/modes/media/media-request-search-group.tsx
new file mode 100644
index 000000000..aa0130a09
--- /dev/null
+++ b/packages/spotlight/src/modes/media/media-request-search-group.tsx
@@ -0,0 +1,173 @@
+import { Badge, Group, Image, Stack, Text } from "@mantine/core";
+import { useDebouncedValue } from "@mantine/hooks";
+import { IconMovie, IconSearch } from "@tabler/icons-react";
+import { keepPreviousData } from "@tanstack/react-query";
+import { useAtomValue } from "jotai";
+
+import type { RouterOutputs } from "@homarr/api";
+import { clientApi } from "@homarr/api/client";
+import { getIntegrationName } from "@homarr/definitions";
+import type { MediaAvailability } from "@homarr/integrations/types";
+import { mediaAvailabilityConfiguration } from "@homarr/integrations/types";
+import { useScopedI18n } from "@homarr/translation/client";
+
+import { createGroup } from "../../lib/group";
+import { mediaRequestSearchScopeAtom } from "../../spotlight-store";
+import { useMediaRequestSearchInteraction } from "../external/search-engines-search-group";
+
+const availabilityLabelKeys = ["available", "partiallyAvailable", "processing", "requested", "pending"] as const;
+type AvailabilityWithLabel = (typeof availabilityLabelKeys)[number];
+
+type MediaRequestSearchResult = RouterOutputs["integration"]["searchMediaRequests"][number];
+
+type MediaRequestSearchOption =
+ | {
+ key: string;
+ kind: "disabled" | "hint";
+ label: string;
+ description: string;
+ }
+ | {
+ key: string;
+ kind: "result";
+ result: MediaRequestSearchResult;
+ };
+
+export const mediaRequestSearchGroup = createGroup({
+ keyPath: "key",
+ title: (t) => t("search.mode.media.group.title"),
+ Component(option) {
+ const tMedia = useScopedI18n("search.mode.media");
+
+ if (option.kind !== "result") {
+ return (
+
+ {option.kind === "disabled" ? : }
+
+ {option.label}
+
+ {option.description}
+
+
+
+ );
+ }
+
+ const { result } = option;
+ const hasAvailabilityLabel =
+ result.availability && (availabilityLabelKeys as readonly string[]).includes(result.availability);
+ const badgeLabel = hasAvailabilityLabel
+ ? tMedia(`availability.${result.availability as AvailabilityWithLabel}`)
+ : undefined;
+ const badgeColor = result.availability
+ ? mediaAvailabilityConfiguration[result.availability as MediaAvailability]?.color
+ : undefined;
+
+ return (
+
+ {result.image ? (
+
+ ) : (
+
+ )}
+
+
+ {result.name}
+ {badgeLabel && (
+
+ {badgeLabel}
+
+ )}
+
+
+ {result.text ?? getIntegrationName(result.integration.kind)}
+
+
+
+ );
+ },
+ useInteraction(option) {
+ if (option.kind !== "result") {
+ return {
+ type: "none",
+ };
+ }
+
+ return useMediaRequestSearchInteraction(option.result.integration, option.result);
+ },
+ useQueryOptions(query) {
+ const tMedia = useScopedI18n("search.mode.media");
+ const scope = useAtomValue(mediaRequestSearchScopeAtom);
+ const [debouncedQuery] = useDebouncedValue(query.trim(), 150);
+ const targetsQuery = clientApi.integration.mediaRequestSearchTargets.useQuery();
+
+ const scopedTargets = (targetsQuery.data ?? []).filter((target) =>
+ scope.integrationIds ? scope.integrationIds.includes(target.id) : true,
+ );
+ const hasScopedTargets = scopedTargets.length > 0;
+ const shouldSearch = debouncedQuery.length > 0 && hasScopedTargets;
+ const integrationIds = scope.integrationIds ? scopedTargets.map((target) => target.id) : undefined;
+
+ const resultsQuery = clientApi.integration.searchMediaRequests.useQuery(
+ { query: debouncedQuery, integrationIds },
+ {
+ enabled: shouldSearch,
+ placeholderData: keepPreviousData,
+ select: (data) => data.slice(0, 10),
+ },
+ );
+
+ const isLoading = targetsQuery.isLoading || (shouldSearch && resultsQuery.isLoading);
+ const isError = targetsQuery.isError || (shouldSearch && resultsQuery.isError);
+
+ if (!targetsQuery.data) {
+ return {
+ isLoading,
+ isError,
+ data: [],
+ };
+ }
+
+ if (!hasScopedTargets) {
+ return {
+ isLoading,
+ isError,
+ data: [
+ {
+ key: "disabled",
+ kind: "disabled",
+ label: tMedia("action.search.label"),
+ description: scope.integrationIds
+ ? tMedia("action.search.disabled.scoped")
+ : tMedia("action.search.disabled.noIntegration"),
+ },
+ ],
+ };
+ }
+
+ if (debouncedQuery.length === 0) {
+ return {
+ isLoading,
+ isError,
+ data: [
+ {
+ key: "hint",
+ kind: "hint",
+ label: tMedia("action.search.label"),
+ description: tMedia("action.search.description"),
+ },
+ ],
+ };
+ }
+
+ return {
+ isLoading,
+ isError,
+ data: (resultsQuery.data ?? []).map((result) => ({
+ key: `result-${result.integration.id}-${result.type}-${result.id}`,
+ kind: "result",
+ result,
+ })),
+ };
+ },
+});
diff --git a/packages/spotlight/src/modes/preferences/groups.tsx b/packages/spotlight/src/modes/preferences/groups.tsx
new file mode 100644
index 000000000..151259d6f
--- /dev/null
+++ b/packages/spotlight/src/modes/preferences/groups.tsx
@@ -0,0 +1,72 @@
+import { Group, Text } from "@mantine/core";
+import { IconSettings } from "@tabler/icons-react";
+
+import { useSession } from "@homarr/auth/client";
+import { colorSchemes } from "@homarr/definitions";
+import { visiblePreferenceDefinitions } from "@homarr/settings";
+import { useI18n, useScopedI18n } from "@homarr/translation/client";
+
+import { createGroup } from "../../lib/group";
+import { preferenceChildrenOptionsByKey, preferenceIcons } from "../../preferences/preference-registry";
+import { settingsChildrenOptions } from "../command/children/preferences/settings-children";
+
+type PreferencesGroupOption = { key: "settings"; preferenceKey: null } | { key: string; preferenceKey: string };
+
+const settingsOption: PreferencesGroupOption = { key: "settings", preferenceKey: null };
+
+export const preferencesGroup = createGroup({
+ keyPath: "key",
+ title: (t) => t("search.mode.command.group.preferences.title"),
+ useOptions(query) {
+ const t = useI18n();
+ const tColorScheme = useScopedI18n("common.colorScheme.options");
+ const { data: session } = useSession();
+ const definitions = visiblePreferenceDefinitions(Boolean(session?.user));
+ const q = query.trim().toLowerCase();
+
+ if (q.length === 0) return [settingsOption];
+
+ const matching = definitions.flatMap((def): PreferencesGroupOption[] => {
+ if (!preferenceChildrenOptionsByKey[def.key]) return [];
+ const label = t(`search.mode.command.group.preferences.option.${def.key}.label` as never);
+ const searchValues = [
+ label,
+ ...def.aliases,
+ ...(def.key === "colorScheme" ? colorSchemes.map((s) => tColorScheme(s)) : []),
+ ];
+ if (!searchValues.some((v) => v.toLowerCase().includes(q))) return [];
+ return [{ key: def.key, preferenceKey: def.key }];
+ });
+
+ if (matching.length > 0) return matching;
+
+ const allSearchValues = [
+ t("search.mode.command.group.preferences.title"),
+ ...definitions.flatMap((d) => d.aliases),
+ ];
+ return allSearchValues.some((v) => v.toLowerCase().includes(q)) ? [settingsOption] : [];
+ },
+ Component: (option) => {
+ const t = useScopedI18n("search.mode.command.group.preferences");
+ const tOption = useScopedI18n("search.mode.command.group.preferences.option");
+ const Icon = option.preferenceKey
+ ? preferenceIcons[option.preferenceKey as keyof typeof preferenceIcons]
+ : IconSettings;
+ const label = option.preferenceKey ? tOption(`${option.preferenceKey}.label` as never) : t("title");
+
+ return (
+
+
+ {label}
+
+ );
+ },
+ filter: () => true,
+ useInteraction: (option) => ({
+ type: "children",
+ ...(option.preferenceKey
+ ? (preferenceChildrenOptionsByKey[option.preferenceKey as keyof typeof preferenceChildrenOptionsByKey]?.({}) ??
+ settingsChildrenOptions({}))
+ : settingsChildrenOptions({})),
+ }),
+});
diff --git a/packages/spotlight/src/preferences/preference-registry.tsx b/packages/spotlight/src/preferences/preference-registry.tsx
new file mode 100644
index 000000000..0a7fbe9df
--- /dev/null
+++ b/packages/spotlight/src/preferences/preference-registry.tsx
@@ -0,0 +1,136 @@
+"use client";
+
+import {
+ IconActivity,
+ IconCalendarWeek,
+ IconDeviceDesktop,
+ IconExternalLink,
+ IconHome,
+ IconLanguage,
+ IconLayoutBoard,
+ IconSearch,
+ IconSettings,
+ IconWorldWww,
+} from "@tabler/icons-react";
+import type { DayOfWeek } from "@mantine/dates";
+import dayjs from "dayjs";
+import localeData from "dayjs/plugin/localeData";
+
+import { clientApi } from "@homarr/api/client";
+import { colorSchemes } from "@homarr/definitions";
+import type { UserPreferenceKey } from "@homarr/settings";
+import { useScopedI18n } from "@homarr/translation/client";
+import type { TablerIcon } from "@homarr/ui";
+
+import { createChildrenOptions } from "../lib/children";
+import {
+ createCheckmarkPreferenceAction,
+ createLoadingPreferenceAction,
+ PreferenceDetailHeader,
+} from "../modes/command/children/preferences/action-row";
+import { createSelectablePreferenceActions } from "../modes/command/children/preferences/selectable-actions";
+import { languageChildrenOptions } from "../modes/command/children/language";
+import { useUserPreference } from "./use-user-preference";
+
+dayjs.extend(localeData);
+
+type ChildrenFactory = ReturnType>>;
+
+export const preferenceIcons: Record = {
+ colorScheme: IconDeviceDesktop,
+ locale: IconLanguage,
+ defaultSearchEngineId: IconSearch,
+ openSearchInNewTab: IconExternalLink,
+ ddgBangs: IconWorldWww,
+ firstDayOfWeek: IconCalendarWeek,
+ homeBoardId: IconHome,
+ mobileHomeBoardId: IconLayoutBoard,
+ pingIconsEnabled: IconActivity,
+ fullPreferencesPage: IconSettings,
+};
+
+const createStaticChildren = (
+ preferenceKey: UserPreferenceKey,
+ useOptions: () => Array<{ key: string; label: string; value: unknown }>,
+): ChildrenFactory =>
+ createChildrenOptions>({
+ useActions: () => {
+ const { value, setValue, isPending } = useUserPreference(preferenceKey);
+ return useOptions().map((opt) =>
+ createCheckmarkPreferenceAction({
+ key: opt.key,
+ label: opt.label,
+ isSelected: value === opt.value,
+ onSelect: () => setValue(opt.value as never),
+ isPending,
+ }),
+ );
+ },
+ DetailComponent: () => ,
+ });
+
+const createAsyncSelectableChildren = (
+ preferenceKey: UserPreferenceKey,
+ useQuery: () => { data: TItem[] | undefined; isLoading: boolean },
+ mapItem: (item: TItem) => { key: string; label: string; value: string },
+ noneKey: string,
+ unavailableKey: string,
+): ChildrenFactory =>
+ createChildrenOptions>({
+ useActions: (_, query) => {
+ const { value, setValue, isPending } = useUserPreference(preferenceKey);
+ const result = useQuery();
+ const t = useScopedI18n("search.mode.command.group.preferences.option");
+
+ if (result.isLoading) return [createLoadingPreferenceAction()];
+
+ return createSelectablePreferenceActions({
+ query,
+ currentValue: value as string | null,
+ noneLabel: t(noneKey as never),
+ unavailableLabel: t(unavailableKey as never),
+ items: (result.data ?? []).map(mapItem),
+ onSelect: (v) => setValue(v as never),
+ isPending,
+ });
+ },
+ DetailComponent: () => ,
+ });
+
+const firstDayOfWeekOptions: DayOfWeek[] = [1, 6, 0];
+
+export const preferenceChildrenOptionsByKey: Partial> = {
+ colorScheme: createStaticChildren("colorScheme", () => {
+ const t = useScopedI18n("common.colorScheme.options");
+ return colorSchemes.map((s) => ({ key: s, label: t(s), value: s }));
+ }),
+ locale: languageChildrenOptions,
+ defaultSearchEngineId: createAsyncSelectableChildren(
+ "defaultSearchEngineId",
+ () => clientApi.searchEngine.getSelectable.useQuery(),
+ (e) => ({ key: e.value, label: e.label, value: e.value }),
+ "searchEngine.none",
+ "searchEngine.unavailable",
+ ),
+ firstDayOfWeek: createStaticChildren("firstDayOfWeek", () =>
+ firstDayOfWeekOptions.map((day) => ({
+ key: day.toString(),
+ label: dayjs.weekdays(false)[day] ?? day.toString(),
+ value: day,
+ })),
+ ),
+ homeBoardId: createAsyncSelectableChildren(
+ "homeBoardId",
+ () => clientApi.board.getAllBoards.useQuery(),
+ (b) => ({ key: b.id, label: b.name, value: b.id }),
+ "board.none",
+ "board.unavailable",
+ ),
+ mobileHomeBoardId: createAsyncSelectableChildren(
+ "mobileHomeBoardId",
+ () => clientApi.board.getAllBoards.useQuery(),
+ (b) => ({ key: b.id, label: b.name, value: b.id }),
+ "board.none",
+ "board.unavailable",
+ ),
+};
diff --git a/packages/spotlight/src/preferences/use-user-preference.ts b/packages/spotlight/src/preferences/use-user-preference.ts
new file mode 100644
index 000000000..81545a5ab
--- /dev/null
+++ b/packages/spotlight/src/preferences/use-user-preference.ts
@@ -0,0 +1,204 @@
+"use client";
+
+import { useEffect, useMemo, useRef, useSyncExternalStore } from "react";
+import { useRouter } from "next/navigation";
+import { useMantineColorScheme } from "@mantine/core";
+import type { DayOfWeek } from "@mantine/dates";
+
+import { clientApi } from "@homarr/api/client";
+import { useSession } from "@homarr/auth/client";
+import type { ColorScheme } from "@homarr/definitions";
+import { showErrorNotification } from "@homarr/notifications";
+import type { SettingsContextProps } from "@homarr/settings/creator";
+import { userPreferenceDefinitions, userPreferenceDefinitionByKey } from "@homarr/settings";
+import type { UserPreferenceKey } from "@homarr/settings";
+import { useSettings } from "@homarr/settings";
+import { useChangeLocale, useCurrentLocale, useI18n } from "@homarr/translation/client";
+
+// --- Optimistic store (module-level, shared across all hook instances) ---
+
+const optimisticRef: { current: Partial> } = { current: {} };
+let optimisticVersion = 0;
+const optimisticListeners = new Set<() => void>();
+
+const notifyListeners = () => {
+ optimisticVersion += 1;
+ optimisticListeners.forEach((fn) => fn());
+};
+
+const subscribeOptimistic = (listener: () => void) => {
+ optimisticListeners.add(listener);
+ return () => optimisticListeners.delete(listener);
+};
+
+const setOptimistic = (key: UserPreferenceKey, value: unknown) => {
+ optimisticRef.current[key] = value;
+ notifyListeners();
+};
+
+const clearOptimistic = (keys: readonly UserPreferenceKey[]) => {
+ let changed = false;
+ for (const key of keys) {
+ changed ||= key in optimisticRef.current;
+ delete optimisticRef.current[key];
+ }
+ if (changed) notifyListeners();
+};
+
+const CLIENT_ONLY_GROUPS = new Set(["colorScheme", "locale"]);
+
+const isDbBacked = (key: UserPreferenceKey) => {
+ const group = userPreferenceDefinitionByKey[key].mutationGroup;
+ return group !== null && !CLIENT_ONLY_GROUPS.has(group);
+};
+
+const getGroupKeys = (key: UserPreferenceKey): UserPreferenceKey[] => {
+ const group = userPreferenceDefinitionByKey[key].mutationGroup;
+ if (!group) return [key];
+ return userPreferenceDefinitions.filter((d) => d.mutationGroup === group).map((d) => d.key);
+};
+
+// --- Hook ---
+
+export const useUserPreferences = () => {
+ const router = useRouter();
+ const settings = useSettings();
+ const settingsRef = useRef(settings);
+ settingsRef.current = settings;
+
+ const { data: session } = useSession();
+ const { colorScheme, setColorScheme } = useMantineColorScheme();
+ const currentLocale = useCurrentLocale();
+ const { changeLocale } = useChangeLocale();
+ const t = useI18n();
+ const userId = session?.user.id;
+ const pendingRef = useRef>({});
+ const prevUserIdRef = useRef(userId);
+
+ useEffect(() => {
+ if (prevUserIdRef.current === userId) return;
+ prevUserIdRef.current = userId;
+ clearOptimistic(userPreferenceDefinitions.map((d) => d.key));
+ }, [userId]);
+
+ useSyncExternalStore(
+ subscribeOptimistic,
+ () => optimisticVersion,
+ () => optimisticVersion,
+ );
+
+ const refresh = () => router.refresh();
+
+ const searchMutation = clientApi.user.changeSearchPreferences.useMutation({ onSuccess: refresh });
+ const homeBoardsMutation = clientApi.user.changeHomeBoards.useMutation({ onSuccess: refresh });
+ const firstDayMutation = clientApi.user.changeFirstDayOfWeek.useMutation({ onSuccess: refresh });
+ const pingMutation = clientApi.user.changePingIconsEnabled.useMutation({ onSuccess: refresh });
+
+ const getEffective = (key: UserPreferenceKey): unknown =>
+ key in optimisticRef.current ? optimisticRef.current[key] : settingsRef.current[key as keyof SettingsContextProps];
+
+ const pick = (field: UserPreferenceKey, changedKey: UserPreferenceKey, value: unknown) =>
+ field === changedKey ? value : getEffective(field);
+
+ const groupDispatchers: Record Promise> = {
+ searchPreferences: (uid, key, value) =>
+ searchMutation.mutateAsync({
+ userId: uid,
+ defaultSearchEngineId: pick("defaultSearchEngineId", key, value) as string | null,
+ openInNewTab: pick("openSearchInNewTab", key, value) as boolean,
+ ddgBangsEnabled: pick("ddgBangs", key, value) as boolean,
+ }),
+ homeBoards: (uid, key, value) =>
+ homeBoardsMutation.mutateAsync({
+ userId: uid,
+ homeBoardId: pick("homeBoardId", key, value) as string | null,
+ mobileHomeBoardId: pick("mobileHomeBoardId", key, value) as string | null,
+ }),
+ firstDayOfWeek: (uid, _key, value) => firstDayMutation.mutateAsync({ id: uid, firstDayOfWeek: value as DayOfWeek }),
+ pingIconsEnabled: (uid, _key, value) => pingMutation.mutateAsync({ id: uid, pingIconsEnabled: value as boolean }),
+ };
+
+ const groupPending: Record = {
+ searchPreferences: searchMutation.isPending,
+ homeBoards: homeBoardsMutation.isPending,
+ firstDayOfWeek: firstDayMutation.isPending,
+ pingIconsEnabled: pingMutation.isPending,
+ };
+
+ const persistedValues = useMemo>(() => {
+ const values: Record = {
+ colorScheme: colorScheme as ColorScheme,
+ locale: currentLocale,
+ fullPreferencesPage: userId ? `/manage/users/${userId}/general` : null,
+ };
+ for (const def of userPreferenceDefinitions) {
+ if (def.key in values) continue;
+ values[def.key] = settings[def.key as keyof SettingsContextProps];
+ }
+ return values as Record;
+ }, [colorScheme, currentLocale, userId, settings]);
+
+ useEffect(() => {
+ const matchedKeys = Object.entries(persistedValues)
+ .filter(([key, value]) => optimisticRef.current[key as UserPreferenceKey] === value)
+ .map(([key]) => key as UserPreferenceKey);
+ clearOptimistic(matchedKeys);
+ }, [persistedValues]);
+
+ const getValue = (key: UserPreferenceKey) =>
+ key in optimisticRef.current ? optimisticRef.current[key] : persistedValues[key];
+
+ const isPreferencePending = (key: UserPreferenceKey): boolean => {
+ const group = userPreferenceDefinitionByKey[key].mutationGroup;
+ if (!group || CLIENT_ONLY_GROUPS.has(group)) return false;
+ return (groupPending[group] ?? false) || (pendingRef.current[group] ?? false);
+ };
+
+ const clientSetters: Partial void>> = {
+ colorScheme: (value) => setColorScheme(value as ColorScheme),
+ locale: (value) => changeLocale(value as typeof currentLocale),
+ fullPreferencesPage: () => undefined,
+ };
+
+ const setValueAsync = async (key: UserPreferenceKey, value: unknown) => {
+ if (isDbBacked(key) && !userId) return;
+ if (isPreferencePending(key)) return;
+
+ const clientSetter = clientSetters[key];
+ if (clientSetter) {
+ clientSetter(value);
+ return;
+ }
+
+ const group = userPreferenceDefinitionByKey[key].mutationGroup;
+ if (!group) return;
+ const dispatcher = groupDispatchers[group];
+ if (!dispatcher || !userId) return;
+
+ setOptimistic(key, value);
+
+ pendingRef.current[group] = true;
+ try {
+ await dispatcher(userId, key, value);
+ } catch {
+ clearOptimistic(getGroupKeys(key));
+ showErrorNotification({
+ message: t("search.mode.command.group.preferences.notification.error.message"),
+ });
+ } finally {
+ pendingRef.current[group] = false;
+ }
+ };
+
+ return {
+ getPreference(preferenceKey: UserPreferenceKey) {
+ return {
+ value: getValue(preferenceKey),
+ setValue: (value: never) => setValueAsync(preferenceKey, value),
+ isPending: isPreferencePending(preferenceKey),
+ };
+ },
+ };
+};
+
+export const useUserPreference = (key: UserPreferenceKey) => useUserPreferences().getPreference(key);
diff --git a/packages/spotlight/src/spotlight-store.ts b/packages/spotlight/src/spotlight-store.ts
index 052ac59cb..a144cdc94 100644
--- a/packages/spotlight/src/spotlight-store.ts
+++ b/packages/spotlight/src/spotlight-store.ts
@@ -3,9 +3,17 @@
import { clamp } from "@mantine/hooks";
import type { SpotlightStore } from "@mantine/spotlight";
import { createSpotlight } from "@mantine/spotlight";
+import { atom } from "jotai";
export const [spotlightStore, spotlightActions] = createSpotlight();
+export interface MediaRequestSearchScope {
+ integrationIds?: string[];
+}
+
+export const mediaRequestSearchScopeAtom = atom({});
+export const mediaRequestSearchEvent = "homarr:spotlight:media-request-search";
+
export const setSelectedAction = (index: number, store: SpotlightStore) => {
store.updateState((state) => ({ ...state, selected: index }));
};
diff --git a/packages/translation/src/lang/ca.json b/packages/translation/src/lang/ca.json
index 3e5c14996..5b9373f4f 100644
--- a/packages/translation/src/lang/ca.json
+++ b/packages/translation/src/lang/ca.json
@@ -132,7 +132,8 @@
},
"password": {
"label": "Contrasenya",
- "requirement": {
+ "suggestionsTitle": "",
+ "suggestion": {
"length": "Inclou com a mínim 8 caràcters",
"lowercase": "Inclou minúscules",
"uppercase": "Inclou majúscules",
@@ -1405,6 +1406,9 @@
"openNewTab": {
"label": ""
},
+ "withBorder": {
+ "label": ""
+ },
"items": {
"label": "",
"add": ""
diff --git a/packages/translation/src/lang/cn.json b/packages/translation/src/lang/cn.json
index 05caccbe2..b3d014d34 100644
--- a/packages/translation/src/lang/cn.json
+++ b/packages/translation/src/lang/cn.json
@@ -132,7 +132,8 @@
},
"password": {
"label": "密码",
- "requirement": {
+ "suggestionsTitle": "",
+ "suggestion": {
"length": "至少包含 8 个字符",
"lowercase": "包含小写字母",
"uppercase": "包含大写字母",
@@ -1405,6 +1406,9 @@
"openNewTab": {
"label": "在新标签页中打开"
},
+ "withBorder": {
+ "label": ""
+ },
"items": {
"label": "书签",
"add": "添加书签"
diff --git a/packages/translation/src/lang/cr.json b/packages/translation/src/lang/cr.json
index ab1fec215..bbda2954c 100644
--- a/packages/translation/src/lang/cr.json
+++ b/packages/translation/src/lang/cr.json
@@ -132,12 +132,13 @@
},
"password": {
"label": "crwdns26:0crwdne26:0",
- "requirement": {
- "length": "crwdns28:0crwdne28:0",
- "lowercase": "crwdns30:0crwdne30:0",
- "uppercase": "crwdns32:0crwdne32:0",
- "number": "crwdns34:0crwdne34:0",
- "special": "crwdns36:0crwdne36:0"
+ "suggestionsTitle": "crwdns4261:0crwdne4261:0",
+ "suggestion": {
+ "length": "crwdns4263:0crwdne4263:0",
+ "lowercase": "crwdns4265:0crwdne4265:0",
+ "uppercase": "crwdns4267:0crwdne4267:0",
+ "number": "crwdns4269:0crwdne4269:0",
+ "special": "crwdns4271:0crwdne4271:0"
}
},
"passwordConfirm": {
@@ -1405,6 +1406,9 @@
"openNewTab": {
"label": "crwdns2686:0crwdne2686:0"
},
+ "withBorder": {
+ "label": "crwdns4273:0crwdne4273:0"
+ },
"items": {
"label": "crwdns764:0crwdne764:0",
"add": "crwdns766:0crwdne766:0"
diff --git a/packages/translation/src/lang/cs.json b/packages/translation/src/lang/cs.json
index 73bd6774f..5ccd16b07 100644
--- a/packages/translation/src/lang/cs.json
+++ b/packages/translation/src/lang/cs.json
@@ -132,7 +132,8 @@
},
"password": {
"label": "Heslo",
- "requirement": {
+ "suggestionsTitle": "",
+ "suggestion": {
"length": "Obsahuje alespoň 8 znaků",
"lowercase": "Obsahuje malé písmeno",
"uppercase": "Obsahuje velké písmeno",
@@ -1405,6 +1406,9 @@
"openNewTab": {
"label": ""
},
+ "withBorder": {
+ "label": ""
+ },
"items": {
"label": "",
"add": ""
diff --git a/packages/translation/src/lang/da.json b/packages/translation/src/lang/da.json
index 25088e56d..c463a155a 100644
--- a/packages/translation/src/lang/da.json
+++ b/packages/translation/src/lang/da.json
@@ -132,7 +132,8 @@
},
"password": {
"label": "Adgangskode",
- "requirement": {
+ "suggestionsTitle": "",
+ "suggestion": {
"length": "Inkluderer mindst 8 tegn",
"lowercase": "Inkluderer små bogstaver",
"uppercase": "Inkluderer store bogstaver",
@@ -1405,6 +1406,9 @@
"openNewTab": {
"label": "Åbn i nyt faneblad"
},
+ "withBorder": {
+ "label": ""
+ },
"items": {
"label": "Bogmærker",
"add": "Tilføj bogmærke"
diff --git a/packages/translation/src/lang/de-CH.json b/packages/translation/src/lang/de-CH.json
index 240cc8a8d..c402186fe 100644
--- a/packages/translation/src/lang/de-CH.json
+++ b/packages/translation/src/lang/de-CH.json
@@ -132,7 +132,8 @@
},
"password": {
"label": "Passwort",
- "requirement": {
+ "suggestionsTitle": "",
+ "suggestion": {
"length": "Enthält mindestens 8 Zeichen",
"lowercase": "Enthält Kleinbuchstaben",
"uppercase": "Enthält Großbuchstaben",
@@ -1405,6 +1406,9 @@
"openNewTab": {
"label": "In neuem Tab öffnen"
},
+ "withBorder": {
+ "label": ""
+ },
"items": {
"label": "Lesezeichen",
"add": "Lesezeichen hinzufügen"
diff --git a/packages/translation/src/lang/de.json b/packages/translation/src/lang/de.json
index 1ac3c8dcb..687e4bc47 100644
--- a/packages/translation/src/lang/de.json
+++ b/packages/translation/src/lang/de.json
@@ -132,7 +132,8 @@
},
"password": {
"label": "Passwort",
- "requirement": {
+ "suggestionsTitle": "",
+ "suggestion": {
"length": "Enthält mindestens 8 Zeichen",
"lowercase": "Enthält Kleinbuchstaben",
"uppercase": "Enthält Großbuchstaben",
@@ -1405,6 +1406,9 @@
"openNewTab": {
"label": "In neuem Tab öffnen"
},
+ "withBorder": {
+ "label": ""
+ },
"items": {
"label": "Lesezeichen",
"add": "Lesezeichen hinzufügen"
diff --git a/packages/translation/src/lang/el.json b/packages/translation/src/lang/el.json
index 2e86228dd..86a250a8f 100644
--- a/packages/translation/src/lang/el.json
+++ b/packages/translation/src/lang/el.json
@@ -132,7 +132,8 @@
},
"password": {
"label": "Κωδικός",
- "requirement": {
+ "suggestionsTitle": "",
+ "suggestion": {
"length": "",
"lowercase": "Περιλαμβάνει πεζό γράμμα",
"uppercase": "Περιλαμβάνει κεφαλαίο γράμμα",
@@ -1405,6 +1406,9 @@
"openNewTab": {
"label": ""
},
+ "withBorder": {
+ "label": ""
+ },
"items": {
"label": "",
"add": ""
diff --git a/packages/translation/src/lang/en-gb.json b/packages/translation/src/lang/en-gb.json
index 04bc5a03c..ecc3d0bb4 100644
--- a/packages/translation/src/lang/en-gb.json
+++ b/packages/translation/src/lang/en-gb.json
@@ -132,7 +132,8 @@
},
"password": {
"label": "Password",
- "requirement": {
+ "suggestionsTitle": "",
+ "suggestion": {
"length": "Includes a minimum of 8 characters",
"lowercase": "Includes a lower-case letter",
"uppercase": "Includes an upper-case letter",
@@ -1405,6 +1406,9 @@
"openNewTab": {
"label": "Open in new tab"
},
+ "withBorder": {
+ "label": ""
+ },
"items": {
"label": "Bookmarks",
"add": "Add bookmark"
@@ -4551,9 +4555,23 @@
}
},
"media": {
+ "help": "Search for movies or shows to request",
"requestMovie": "",
"requestSeries": "",
- "openIn": ""
+ "openIn": "",
+ "group": {
+ "title": "Movies and shows"
+ },
+ "action": {
+ "search": {
+ "label": "Search for movies or shows",
+ "description": "Start typing to search your media request integrations",
+ "disabled": {
+ "noIntegration": "Add an Overseerr, Jellyseerr, or Seerr integration to enable media search.",
+ "scoped": "This widget does not have an accessible media request integration."
+ }
+ }
+ }
},
"external": {
"help": "",
@@ -4829,7 +4847,11 @@
"table": {
"header": {
"season": "Season",
- "episodes": "Episodes"
+ "episodes": "Episodes",
+ "status": "Status"
+ },
+ "status": {
+ "requested": "Requested"
}
},
"button": {
diff --git a/packages/translation/src/lang/en.json b/packages/translation/src/lang/en.json
index a62728845..2a2de758b 100644
--- a/packages/translation/src/lang/en.json
+++ b/packages/translation/src/lang/en.json
@@ -90,6 +90,48 @@
"title": "Settings",
"subtitle": "Configure server settings."
},
+ "integrations": {
+ "title": "Integrations",
+ "subtitle": "Select the services you use.",
+ "description": "Choose the integrations you'd like to connect. We'll set up your dashboard with matching widgets automatically.",
+ "action": {
+ "continue": "Continue",
+ "skip": "Skip for now"
+ },
+ "configure": {
+ "progress": "{current} of {total}",
+ "skip": "Skip this integration",
+ "finishing": "Setting up your dashboard...",
+ "error": {
+ "title": "Setup failed",
+ "message": "Something went wrong while setting up your dashboard. Please try again."
+ },
+ "helpers": {
+ "tooltip": "URL helpers",
+ "description": "These helpers try to guess the URL for your services. If it doesn't work, just type the address manually."
+ }
+ },
+ "urlTemplate": {
+ "label": "Base URL",
+ "placeholder": "homelab.local or 192.168.1.1",
+ "hint": "Enter your base domain or IP to auto-fill integration URLs",
+ "warning": "Setting your base URL now will auto-fill all integration URLs, saving you time during setup",
+ "preview": "e.g. {example}",
+ "mode": {
+ "subdomain": "Subdomain",
+ "hostPort": "Host:Port"
+ }
+ },
+ "docker": {
+ "detected": "Docker detected {count} services",
+ "detectedBreakdown": "{integrations} integrations · {apps} apps",
+ "summary": "Homarr detected {apps} apps, {integrations} of which have integrations built-in",
+ "appBadge": "App",
+ "appSectionLabel": "Apps to add to dashboard ({count} selected)",
+ "scanning": "Scanning Docker...",
+ "scanningDescription": "Looking for running containers on your system"
+ }
+ },
"finish": {
"title": "Finish setup",
"subtitle": "You are ready to go!",
@@ -132,8 +174,9 @@
},
"password": {
"label": "Password",
- "requirement": {
- "length": "Includes at least 8 characters",
+ "suggestionsTitle": "Password suggestions",
+ "suggestion": {
+ "length": "At least 8 characters",
"lowercase": "Includes lowercase letter",
"uppercase": "Includes uppercase letter",
"number": "Includes number",
@@ -730,11 +773,46 @@
},
"appHref": {
"placeholder": "Custom app URL"
+ },
+ "apiKeySettings": {
+ "label": "Get API key"
}
},
"action": {
"create": "New integration"
},
+ "grid": {
+ "widgetCount": "{count} widgets",
+ "noWidgets": "No widgets",
+ "detected": "Detected via Docker"
+ },
+ "category": {
+ "dnsHole": "DNS",
+ "mediaService": "Media",
+ "calendar": "Calendar",
+ "mediaSearch": "Search",
+ "mediaRelease": "Releases",
+ "mediaRequest": "Requests",
+ "downloadClient": "Downloads",
+ "usenet": "Usenet",
+ "torrent": "Torrent",
+ "miscellaneous": "Misc",
+ "smartHomeServer": "Smart Home",
+ "indexerManager": "Indexer",
+ "healthMonitoring": "Health",
+ "search": "Search",
+ "mediaTranscoding": "Transcoding",
+ "networkController": "Network",
+ "releasesProvider": "Releases",
+ "notifications": "Notifications",
+ "firewall": "Firewall",
+ "timetable": "Timetable",
+ "photoService": "Photos",
+ "notes": "Notes",
+ "mediaMonitoring": "Monitoring",
+ "speedtest": "Speed Test",
+ "analytics": "Analytics"
+ },
"testConnection": {
"action": {
"create": "Test connection and create",
@@ -1067,6 +1145,7 @@
"add": "Add",
"apply": "Apply",
"backToOverview": "Back to overview",
+ "skip": "Skip",
"change": "Change",
"create": "Create",
"createAnother": "Create and start over",
@@ -1302,6 +1381,13 @@
},
"edit": {
"title": "Edit item",
+ "tab": {
+ "widget": "Edit widget",
+ "app": "Edit app"
+ },
+ "app": {
+ "propagationNotice": "Changes to this app will be applied everywhere it is used across all dashboards."
+ },
"advancedOptions": {
"label": "Advanced options",
"title": "Advanced item options"
@@ -1405,6 +1491,9 @@
"openNewTab": {
"label": "Open in new tab"
},
+ "withBorder": {
+ "label": "Show border"
+ },
"items": {
"label": "Bookmarks",
"add": "Add bookmark"
@@ -2061,6 +2150,37 @@
"title": "Instances"
}
},
+ "ups": {
+ "name": "UPS",
+ "description": "Monitor the status of your UPS devices through a NUT server",
+ "option": {
+ "showBattery": {
+ "label": "Show battery"
+ },
+ "showLoad": {
+ "label": "Show load"
+ },
+ "showVoltage": {
+ "label": "Show voltage"
+ }
+ },
+ "status": {
+ "online": "Online",
+ "onBattery": "On battery",
+ "lowBattery": "Low battery",
+ "charging": "Charging",
+ "unknown": "Unknown"
+ },
+ "field": {
+ "runtime": "Runtime",
+ "load": "Load",
+ "input": "Input",
+ "output": "Output"
+ },
+ "error": {
+ "internalServerError": "Failed to fetch UPS data"
+ }
+ },
"tracearr": {
"name": "Tracearr",
"description": "Enhanced media server monitoring with detailed stream and transcoding data",
@@ -3491,7 +3611,24 @@
},
"search": "Search",
"firstDayOfWeek": "First day of the week",
- "accessibility": "Accessibility"
+ "accessibility": "Accessibility",
+ "onboardingTours": {
+ "title": "Onboarding Tours",
+ "description": "Reset the onboarding tours to see them again on your next visit to the management area or dashboard.",
+ "reset": "Reset onboarding tours",
+ "resetSuccess": {
+ "title": "Tours reset",
+ "message": "Onboarding tours will show again on your next visit"
+ },
+ "status": {
+ "completed": "Completed",
+ "notStarted": "Not started"
+ },
+ "name": {
+ "management": "Management",
+ "dashboard": "Dashboard"
+ }
+ }
}
},
"security": {
@@ -3986,6 +4123,12 @@
},
"action": {
"title": "Actions",
+ "logs": {
+ "label": "View logs",
+ "page": {
+ "title": "{name} logs"
+ }
+ },
"start": {
"label": "Start",
"notification": {
@@ -4398,7 +4541,10 @@
"label": "Tasks"
},
"docker": {
- "label": "Docker"
+ "label": "Docker",
+ "logs": {
+ "label": "Logs"
+ }
},
"kubernetes": {
"label": "Kubernetes",
@@ -4547,13 +4693,141 @@
"label": "Create a new group"
}
}
+ },
+ "preferences": {
+ "title": "Preferences",
+ "children": {
+ "detail": {
+ "title": "Select a preference to change",
+ "backHint": "Press Backspace or use the back button to go back",
+ "backAction": "Go back"
+ }
+ },
+ "notification": {
+ "error": {
+ "message": "Unable to update preference"
+ }
+ },
+ "option": {
+ "preferences": {
+ "label": "Open preferences"
+ },
+ "boolean": {
+ "enabled": "Enabled",
+ "disabled": "Disabled"
+ },
+ "board": {
+ "none": "None",
+ "unavailable": "Currently selected (unavailable)"
+ },
+ "searchEngine": {
+ "none": "None",
+ "unavailable": "Currently selected (unavailable)"
+ },
+ "colorScheme": {
+ "label": "Color scheme",
+ "children": {
+ "detail": {
+ "title": "Select your color scheme"
+ }
+ }
+ },
+ "locale": {
+ "label": "Language",
+ "children": {
+ "detail": {
+ "title": "Select your preferred language"
+ }
+ }
+ },
+ "defaultSearchEngineId": {
+ "label": "Default search engine",
+ "children": {
+ "detail": {
+ "title": "Select your default search engine"
+ }
+ }
+ },
+ "openSearchInNewTab": {
+ "label": "Open search results in new tab",
+ "children": {
+ "detail": {
+ "title": "Open search results in new tab"
+ }
+ }
+ },
+ "ddgBangs": {
+ "label": "Enable DuckDuckGo bangs",
+ "children": {
+ "detail": {
+ "title": "Enable DuckDuckGo bangs"
+ }
+ }
+ },
+ "firstDayOfWeek": {
+ "label": "First day of the week",
+ "children": {
+ "detail": {
+ "title": "Select the first day of the week"
+ }
+ }
+ },
+ "homeBoardId": {
+ "label": "Home board",
+ "children": {
+ "detail": {
+ "title": "Select your home board"
+ }
+ }
+ },
+ "mobileHomeBoardId": {
+ "label": "Mobile home board",
+ "children": {
+ "detail": {
+ "title": "Select your mobile home board"
+ }
+ }
+ },
+ "pingIconsEnabled": {
+ "label": "Use icons for pings",
+ "children": {
+ "detail": {
+ "title": "Use icons for pings"
+ }
+ }
+ },
+ "fullPreferencesPage": {
+ "label": "Open full preferences page"
+ }
+ }
}
}
},
"media": {
+ "help": "Search for movies or shows to request",
"requestMovie": "Request movie",
"requestSeries": "Request series",
- "openIn": "Open in {kind}"
+ "openIn": "Open in {kind}",
+ "group": {
+ "title": "Movies and shows"
+ },
+ "action": {
+ "search": {
+ "label": "Search for movies or shows",
+ "description": "Start typing to search your media request integrations",
+ "disabled": {
+ "noIntegration": "Add an Overseerr, Jellyseerr, or Seerr integration to enable media search.",
+ "scoped": "This widget does not have an accessible media request integration."
+ }
+ }
+ },
+ "availability": {
+ "available": "Available",
+ "partiallyAvailable": "Partial",
+ "processing": "Processing",
+ "requested": "Requested",
+ "pending": "Requested"
+ }
},
"external": {
"help": "Use an external search engine",
@@ -4829,7 +5103,11 @@
"table": {
"header": {
"season": "Season",
- "episodes": "Episodes"
+ "episodes": "Episodes",
+ "status": "Status"
+ },
+ "status": {
+ "requested": "Requested"
}
},
"button": {
@@ -4940,5 +5218,72 @@
"error": "Error"
}
}
+ },
+ "onboardingTour": {
+ "learnMore": "Read the docs",
+ "next": "Next",
+ "prev": "Back",
+ "done": "Done",
+ "skip": "Skip tour",
+ "manage": {
+ "welcome": {
+ "title": "Welcome to Homarr!",
+ "description": "This is your control center. From here you manage boards, apps, integrations, users, and server settings. Let's take a tour of each section."
+ },
+ "boardsList": {
+ "title": "Your Boards",
+ "description": "Here are all your dashboards. Each board has its own layout, background, and access settings. You can see who created each one and whether it's public or private."
+ },
+ "boardsCreate": {
+ "title": "Create a Board",
+ "description": "Click here to create a new dashboard or import one from a previous Homarr installation. Each board gets its own URL you can share."
+ },
+ "appsList": {
+ "title": "Your Apps",
+ "description": "These are all the apps you've added — links to your self-hosted services. Each app has an icon, optional health check, and a status badge on the dashboard."
+ },
+ "appsCreate": {
+ "title": "Add an App",
+ "description": "Register a new service here. Give it a name, URL, and icon — it will appear as a tile on your boards with live status monitoring."
+ },
+ "integrationsList": {
+ "title": "Your Integrations",
+ "description": "These are the services connected to Homarr — Sonarr, Radarr, Plex, Jellyfin, and 50+ more. All credentials are encrypted at rest."
+ },
+ "integrationsCreate": {
+ "title": "Add an Integration",
+ "description": "Connect a new service here. Pick the type, enter the URL and API key — Homarr validates the connection before saving."
+ },
+ "usersList": {
+ "title": "Your Users",
+ "description": "See everyone who has access. Organize users into groups and control who can view or edit each board."
+ },
+ "usersCreate": {
+ "title": "Create a User",
+ "description": "Add a new user account here. You can also use invite links for easy onboarding. That's the tour — you're all set!"
+ }
+ },
+ "board": {
+ "welcome": {
+ "title": "This is your board",
+ "description": "A drag-and-drop dashboard where you arrange apps and widgets. Press Ctrl+K anytime to search for apps, boards, and actions."
+ },
+ "editMode": {
+ "title": "Edit Mode",
+ "description": "Click here to enter edit mode. You can then add widgets, apps, and sections — drag to reposition, resize with the corner handle. Click again to save your changes."
+ },
+ "settings": {
+ "title": "Board Settings",
+ "description": "Change this board's name, columns, background, colors, and who can access it. Each board can be public or private."
+ },
+ "switcher": {
+ "title": "Switch Boards",
+ "description": "Jump between boards. Create separate dashboards for different purposes — media, networking, per-user, or anything you like."
+ },
+ "userMenu": {
+ "title": "Your Profile",
+ "description": "Change your color scheme, language, home board, and preferences. You can also jump to the management area from here."
+ }
+ }
}
}
diff --git a/packages/translation/src/lang/es.json b/packages/translation/src/lang/es.json
index 148c022dd..08be259ed 100644
--- a/packages/translation/src/lang/es.json
+++ b/packages/translation/src/lang/es.json
@@ -132,7 +132,8 @@
},
"password": {
"label": "Contraseña",
- "requirement": {
+ "suggestionsTitle": "",
+ "suggestion": {
"length": "Incluye al menos 8 caracteres",
"lowercase": "Incluye al menos una letra minúscula",
"uppercase": "Incluye al menos una letra mayúscula",
@@ -1405,6 +1406,9 @@
"openNewTab": {
"label": "Abrir en una nueva pestaña"
},
+ "withBorder": {
+ "label": ""
+ },
"items": {
"label": "Marcadores",
"add": "Añadir marcador"
diff --git a/packages/translation/src/lang/et.json b/packages/translation/src/lang/et.json
index c5e54f495..d32700a60 100644
--- a/packages/translation/src/lang/et.json
+++ b/packages/translation/src/lang/et.json
@@ -132,7 +132,8 @@
},
"password": {
"label": "",
- "requirement": {
+ "suggestionsTitle": "",
+ "suggestion": {
"length": "",
"lowercase": "",
"uppercase": "",
@@ -1405,6 +1406,9 @@
"openNewTab": {
"label": ""
},
+ "withBorder": {
+ "label": ""
+ },
"items": {
"label": "",
"add": ""
diff --git a/packages/translation/src/lang/fr.json b/packages/translation/src/lang/fr.json
index 2f173a25c..0c3952ba5 100644
--- a/packages/translation/src/lang/fr.json
+++ b/packages/translation/src/lang/fr.json
@@ -132,7 +132,8 @@
},
"password": {
"label": "Mot de passe",
- "requirement": {
+ "suggestionsTitle": "",
+ "suggestion": {
"length": "Contient au moins 8 caractères",
"lowercase": "Inclut une lettre minuscule",
"uppercase": "Inclut une lettre majuscule",
@@ -1405,6 +1406,9 @@
"openNewTab": {
"label": "Ouvrir dans un nouvel onglet"
},
+ "withBorder": {
+ "label": ""
+ },
"items": {
"label": "Signets",
"add": "Ajouter un signet"
@@ -2088,7 +2092,7 @@
"empty": "",
"user": "Utilisateur",
"media": "Média",
- "state": "",
+ "state": "État",
"quality": "Qualité",
"device": "Appareil",
"progress": "Progression",
diff --git a/packages/translation/src/lang/he.json b/packages/translation/src/lang/he.json
index 86c2e8ee3..dcff4afed 100644
--- a/packages/translation/src/lang/he.json
+++ b/packages/translation/src/lang/he.json
@@ -132,7 +132,8 @@
},
"password": {
"label": "סיסמה",
- "requirement": {
+ "suggestionsTitle": "",
+ "suggestion": {
"length": "כולל לפחות 8 תווים",
"lowercase": "אפשר אותיות קטנות",
"uppercase": "אפשר אותיות גדולות",
@@ -1405,6 +1406,9 @@
"openNewTab": {
"label": "פתיחה בכרטיסיה חדשה"
},
+ "withBorder": {
+ "label": ""
+ },
"items": {
"label": "סימניות",
"add": "הוסף סימניה"
diff --git a/packages/translation/src/lang/hr.json b/packages/translation/src/lang/hr.json
index dd637c9a6..5ae2807f6 100644
--- a/packages/translation/src/lang/hr.json
+++ b/packages/translation/src/lang/hr.json
@@ -132,7 +132,8 @@
},
"password": {
"label": "Lozinka",
- "requirement": {
+ "suggestionsTitle": "",
+ "suggestion": {
"length": "",
"lowercase": "Uključuje mala slova",
"uppercase": "Uključuje veliko slovo",
@@ -1405,6 +1406,9 @@
"openNewTab": {
"label": ""
},
+ "withBorder": {
+ "label": ""
+ },
"items": {
"label": "",
"add": ""
diff --git a/packages/translation/src/lang/hu.json b/packages/translation/src/lang/hu.json
index 1bad936e8..8deb2b35c 100644
--- a/packages/translation/src/lang/hu.json
+++ b/packages/translation/src/lang/hu.json
@@ -132,7 +132,8 @@
},
"password": {
"label": "Jelszó",
- "requirement": {
+ "suggestionsTitle": "",
+ "suggestion": {
"length": "Tartalmazzon legalább 8 karaktert",
"lowercase": "Tartalmazzon kisbetűt",
"uppercase": "Tartalmazzon nagybetűt",
@@ -1405,6 +1406,9 @@
"openNewTab": {
"label": "Megnyitás új lapon"
},
+ "withBorder": {
+ "label": ""
+ },
"items": {
"label": "Könyjelzők",
"add": "Könyvjelző hozzáadása"
diff --git a/packages/translation/src/lang/it.json b/packages/translation/src/lang/it.json
index 7a2202367..c4bebce98 100644
--- a/packages/translation/src/lang/it.json
+++ b/packages/translation/src/lang/it.json
@@ -132,7 +132,8 @@
},
"password": {
"label": "Password",
- "requirement": {
+ "suggestionsTitle": "Suggerimenti password",
+ "suggestion": {
"length": "Include almeno 8 caratteri",
"lowercase": "Include lettera minuscola",
"uppercase": "Include lettera maiuscola",
@@ -1056,7 +1057,7 @@
}
},
"noResults": {
- "title": ""
+ "title": "Nessun media caricato"
}
},
"common": {
@@ -1088,8 +1089,8 @@
"checkLogs": "Controlla i log per maggiori dettagli",
"tryAgain": "Riprova",
"loading": "Caricamento",
- "show": "",
- "hide": ""
+ "show": "Visualizza",
+ "hide": "Nascondi"
},
"here": "qui",
"iconPicker": {
@@ -1255,7 +1256,7 @@
"label": {
"create": "Nuova categoria",
"changePosition": "Cambia posizione",
- "settings": ""
+ "settings": "Impostazioni"
}
},
"openAllInNewTabs": {
@@ -1405,6 +1406,9 @@
"openNewTab": {
"label": "Apri in una nuova scheda"
},
+ "withBorder": {
+ "label": "Mostra bordo"
+ },
"items": {
"label": "Segnalibri",
"add": "Aggiungi segnalibro"
@@ -3459,7 +3463,7 @@
"private": "Questa board è privata"
},
"noResults": {
- "title": ""
+ "title": "Ancora nessuna board"
},
"modal": {
"createBoard": {
diff --git a/packages/translation/src/lang/ja.json b/packages/translation/src/lang/ja.json
index aacb952c6..606e3024d 100644
--- a/packages/translation/src/lang/ja.json
+++ b/packages/translation/src/lang/ja.json
@@ -132,7 +132,8 @@
},
"password": {
"label": "パスワード",
- "requirement": {
+ "suggestionsTitle": "",
+ "suggestion": {
"length": "少なくとも 8 文字以上を含む",
"lowercase": "小文字を含む",
"uppercase": "大文字を含む",
@@ -1405,6 +1406,9 @@
"openNewTab": {
"label": "新しいタブで開く"
},
+ "withBorder": {
+ "label": ""
+ },
"items": {
"label": "ブックマーク",
"add": "ブックマークに追加"
diff --git a/packages/translation/src/lang/ko.json b/packages/translation/src/lang/ko.json
index 6a8639ffd..3dc98b4c7 100644
--- a/packages/translation/src/lang/ko.json
+++ b/packages/translation/src/lang/ko.json
@@ -132,7 +132,8 @@
},
"password": {
"label": "비밀번호",
- "requirement": {
+ "suggestionsTitle": "",
+ "suggestion": {
"length": "",
"lowercase": "소문자 포함",
"uppercase": "대문자 포함",
@@ -1405,6 +1406,9 @@
"openNewTab": {
"label": ""
},
+ "withBorder": {
+ "label": ""
+ },
"items": {
"label": "",
"add": ""
diff --git a/packages/translation/src/lang/lt.json b/packages/translation/src/lang/lt.json
index 6811b62e5..fd1099a91 100644
--- a/packages/translation/src/lang/lt.json
+++ b/packages/translation/src/lang/lt.json
@@ -132,7 +132,8 @@
},
"password": {
"label": "Slaptažodis",
- "requirement": {
+ "suggestionsTitle": "",
+ "suggestion": {
"length": "",
"lowercase": "",
"uppercase": "",
@@ -1405,6 +1406,9 @@
"openNewTab": {
"label": ""
},
+ "withBorder": {
+ "label": ""
+ },
"items": {
"label": "",
"add": ""
diff --git a/packages/translation/src/lang/lv.json b/packages/translation/src/lang/lv.json
index e3afee8da..465318eed 100644
--- a/packages/translation/src/lang/lv.json
+++ b/packages/translation/src/lang/lv.json
@@ -132,7 +132,8 @@
},
"password": {
"label": "Parole",
- "requirement": {
+ "suggestionsTitle": "",
+ "suggestion": {
"length": "",
"lowercase": "Ietver mazo burtu",
"uppercase": "Ietver lielo burtu",
@@ -1405,6 +1406,9 @@
"openNewTab": {
"label": ""
},
+ "withBorder": {
+ "label": ""
+ },
"items": {
"label": "",
"add": ""
diff --git a/packages/translation/src/lang/nl.json b/packages/translation/src/lang/nl.json
index bf16ada09..ab195e752 100644
--- a/packages/translation/src/lang/nl.json
+++ b/packages/translation/src/lang/nl.json
@@ -132,7 +132,8 @@
},
"password": {
"label": "Wachtwoord",
- "requirement": {
+ "suggestionsTitle": "",
+ "suggestion": {
"length": "Bevat ten minste 8 tekens",
"lowercase": "Inclusief kleine letter",
"uppercase": "Inclusief hoofdletter",
@@ -1405,6 +1406,9 @@
"openNewTab": {
"label": "In nieuw tabblad openen"
},
+ "withBorder": {
+ "label": ""
+ },
"items": {
"label": "Bladwijzers",
"add": "Bladwijzer toevoegen"
diff --git a/packages/translation/src/lang/no.json b/packages/translation/src/lang/no.json
index 4badb0f64..079f671f5 100644
--- a/packages/translation/src/lang/no.json
+++ b/packages/translation/src/lang/no.json
@@ -132,7 +132,8 @@
},
"password": {
"label": "Passord",
- "requirement": {
+ "suggestionsTitle": "",
+ "suggestion": {
"length": "Inkluderer minst 8 tegn",
"lowercase": "Inkluderer liten bokstav",
"uppercase": "Inkluderer stor bokstav",
@@ -1405,6 +1406,9 @@
"openNewTab": {
"label": "Åpne i ny fane"
},
+ "withBorder": {
+ "label": ""
+ },
"items": {
"label": "Bokmerke",
"add": "Legg til bokmerke"
diff --git a/packages/translation/src/lang/pl.json b/packages/translation/src/lang/pl.json
index ead2f3653..3ebc342b0 100644
--- a/packages/translation/src/lang/pl.json
+++ b/packages/translation/src/lang/pl.json
@@ -132,7 +132,8 @@
},
"password": {
"label": "Hasło",
- "requirement": {
+ "suggestionsTitle": "",
+ "suggestion": {
"length": "Zawiera co najmniej 8 znaków",
"lowercase": "Zawiera małą literę",
"uppercase": "Zawiera wielką literę",
@@ -1405,6 +1406,9 @@
"openNewTab": {
"label": "Otwórz w nowej karcie"
},
+ "withBorder": {
+ "label": ""
+ },
"items": {
"label": "Zakładki",
"add": "Dodaj zakładkę"
diff --git a/packages/translation/src/lang/pt-br.json b/packages/translation/src/lang/pt-br.json
index a436eac1d..097a72f41 100644
--- a/packages/translation/src/lang/pt-br.json
+++ b/packages/translation/src/lang/pt-br.json
@@ -132,7 +132,8 @@
},
"password": {
"label": "Senha",
- "requirement": {
+ "suggestionsTitle": "",
+ "suggestion": {
"length": "Inclua pelo menos 8 caracteres",
"lowercase": "Inclui letras minúsculas",
"uppercase": "Inclui letras maiúsculas",
@@ -1056,7 +1057,7 @@
}
},
"noResults": {
- "title": ""
+ "title": "Nenhuma mídia enviada"
}
},
"common": {
@@ -1088,8 +1089,8 @@
"checkLogs": "Verifique os logs para mais detalhes",
"tryAgain": "Tente novamente",
"loading": "Carregando",
- "show": "",
- "hide": ""
+ "show": "Mostrar",
+ "hide": "Esconder"
},
"here": "aqui",
"iconPicker": {
@@ -1255,7 +1256,7 @@
"label": {
"create": "Nova categoria",
"changePosition": "Mudar de posição",
- "settings": ""
+ "settings": "Configurações"
}
},
"openAllInNewTabs": {
@@ -1405,6 +1406,9 @@
"openNewTab": {
"label": "Abrir em nova guia"
},
+ "withBorder": {
+ "label": ""
+ },
"items": {
"label": "Marcadores",
"add": "Adicionar marcador"
@@ -3459,7 +3463,7 @@
"private": "Este quadro é privado"
},
"noResults": {
- "title": ""
+ "title": "Nenhum painel ainda"
},
"modal": {
"createBoard": {
diff --git a/packages/translation/src/lang/pt.json b/packages/translation/src/lang/pt.json
index 5d749ae0d..d5882e121 100644
--- a/packages/translation/src/lang/pt.json
+++ b/packages/translation/src/lang/pt.json
@@ -132,7 +132,8 @@
},
"password": {
"label": "",
- "requirement": {
+ "suggestionsTitle": "",
+ "suggestion": {
"length": "",
"lowercase": "",
"uppercase": "",
@@ -1405,6 +1406,9 @@
"openNewTab": {
"label": ""
},
+ "withBorder": {
+ "label": ""
+ },
"items": {
"label": "",
"add": ""
diff --git a/packages/translation/src/lang/ro.json b/packages/translation/src/lang/ro.json
index d4244df64..e604bd831 100644
--- a/packages/translation/src/lang/ro.json
+++ b/packages/translation/src/lang/ro.json
@@ -132,7 +132,8 @@
},
"password": {
"label": "Parola",
- "requirement": {
+ "suggestionsTitle": "",
+ "suggestion": {
"length": "",
"lowercase": "Să includă litere mici",
"uppercase": "Să includă litere mari",
@@ -1405,6 +1406,9 @@
"openNewTab": {
"label": ""
},
+ "withBorder": {
+ "label": ""
+ },
"items": {
"label": "",
"add": ""
diff --git a/packages/translation/src/lang/ru.json b/packages/translation/src/lang/ru.json
index 2be2fdabb..2bb123d64 100644
--- a/packages/translation/src/lang/ru.json
+++ b/packages/translation/src/lang/ru.json
@@ -132,7 +132,8 @@
},
"password": {
"label": "Пароль",
- "requirement": {
+ "suggestionsTitle": "",
+ "suggestion": {
"length": "Содержит не менее 8 символов",
"lowercase": "Содержит строчную букву",
"uppercase": "Содержит заглавную букву",
@@ -1405,6 +1406,9 @@
"openNewTab": {
"label": "Открывать в новой вкладке"
},
+ "withBorder": {
+ "label": ""
+ },
"items": {
"label": "Закладки",
"add": "Добавить закладку"
diff --git a/packages/translation/src/lang/sk.json b/packages/translation/src/lang/sk.json
index bc5796623..5da033f84 100644
--- a/packages/translation/src/lang/sk.json
+++ b/packages/translation/src/lang/sk.json
@@ -132,7 +132,8 @@
},
"password": {
"label": "Heslo",
- "requirement": {
+ "suggestionsTitle": "",
+ "suggestion": {
"length": "Obsahuje aspoň 8 znakov",
"lowercase": "Zahŕňa malé písmeno",
"uppercase": "Zahŕňa veľké písmená",
@@ -1405,6 +1406,9 @@
"openNewTab": {
"label": "Otvoriť na novej karte"
},
+ "withBorder": {
+ "label": ""
+ },
"items": {
"label": "Záložky",
"add": "Pridať záložku"
diff --git a/packages/translation/src/lang/sl.json b/packages/translation/src/lang/sl.json
index 51c078175..df14b4a8d 100644
--- a/packages/translation/src/lang/sl.json
+++ b/packages/translation/src/lang/sl.json
@@ -132,7 +132,8 @@
},
"password": {
"label": "Geslo",
- "requirement": {
+ "suggestionsTitle": "",
+ "suggestion": {
"length": "",
"lowercase": "Vključuje male črke",
"uppercase": "Vključuje velike tiskane črke",
@@ -1405,6 +1406,9 @@
"openNewTab": {
"label": ""
},
+ "withBorder": {
+ "label": ""
+ },
"items": {
"label": "",
"add": ""
diff --git a/packages/translation/src/lang/sv.json b/packages/translation/src/lang/sv.json
index 20301caf2..7dd838999 100644
--- a/packages/translation/src/lang/sv.json
+++ b/packages/translation/src/lang/sv.json
@@ -82,7 +82,7 @@
"form": {
"name": {
"label": "Gruppens namn",
- "description": ""
+ "description": "Namnet måste matcha admin-grupp i extern tjänst"
}
}
},
@@ -132,7 +132,8 @@
},
"password": {
"label": "Lösenord",
- "requirement": {
+ "suggestionsTitle": "",
+ "suggestion": {
"length": "Använd minst 8 tecken",
"lowercase": "Använd liten bokstav",
"uppercase": "Använd stor bokstav",
@@ -159,7 +160,7 @@
"label": "Visa sökresultat i ny flik"
},
"ddgBangs": {
- "label": ""
+ "label": "Aktivera DuckDuckGo bangs"
}
},
"error": {
@@ -252,10 +253,10 @@
"changeDdgBangs": {
"notification": {
"success": {
- "message": ""
+ "message": "DuckDuckGo bangs inställningen ändrad"
},
"error": {
- "message": ""
+ "message": "DuckDuckGo bangs inställningen kunde inte ändras"
}
}
},
@@ -350,12 +351,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 +364,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 +393,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 +436,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. "
},
"action": {
"create": {
"label": "Addera en grupp",
"notification": {
"success": {
- "message": ""
+ "message": "Gruppen har skapats"
},
"error": {
- "message": ""
+ "message": "Gruppen kunde inte skapas"
}
}
},
@@ -470,7 +471,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 +502,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 +541,10 @@
"changePosition": {
"notification": {
"success": {
- "message": ""
+ "message": "Ordning ändrad"
},
"error": {
- "message": ""
+ "message": "Ordningen kunde ändras"
}
}
}
@@ -559,20 +560,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 +582,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 +595,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 +613,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 +643,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 +701,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 +716,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": "",
@@ -1405,6 +1406,9 @@
"openNewTab": {
"label": "Öppna i en ny flik"
},
+ "withBorder": {
+ "label": ""
+ },
"items": {
"label": "Bokmärken",
"add": "Addera bokmärke"
diff --git a/packages/translation/src/lang/tr.json b/packages/translation/src/lang/tr.json
index 8b622b9dd..645768750 100644
--- a/packages/translation/src/lang/tr.json
+++ b/packages/translation/src/lang/tr.json
@@ -132,7 +132,8 @@
},
"password": {
"label": "Şifre",
- "requirement": {
+ "suggestionsTitle": "",
+ "suggestion": {
"length": "En az 8 karakter içermeli",
"lowercase": "Küçük harf içermeli",
"uppercase": "Büyük harf içermeli",
@@ -1405,6 +1406,9 @@
"openNewTab": {
"label": "Yeni sekmede aç"
},
+ "withBorder": {
+ "label": ""
+ },
"items": {
"label": "Yer İmleri",
"add": "Yer imi ekle"
diff --git a/packages/translation/src/lang/uk.json b/packages/translation/src/lang/uk.json
index 9d08afe64..4ccf9d0b7 100644
--- a/packages/translation/src/lang/uk.json
+++ b/packages/translation/src/lang/uk.json
@@ -132,7 +132,8 @@
},
"password": {
"label": "Пароль",
- "requirement": {
+ "suggestionsTitle": "",
+ "suggestion": {
"length": "Включає принаймні 8 символів",
"lowercase": "Включає малу літеру",
"uppercase": "Включає велику літеру",
@@ -1405,6 +1406,9 @@
"openNewTab": {
"label": "Відкрити в новій вкладці"
},
+ "withBorder": {
+ "label": ""
+ },
"items": {
"label": "Закладки",
"add": "Додати закладку"
diff --git a/packages/translation/src/lang/vi.json b/packages/translation/src/lang/vi.json
index a9466e9c8..e16d468c1 100644
--- a/packages/translation/src/lang/vi.json
+++ b/packages/translation/src/lang/vi.json
@@ -132,7 +132,8 @@
},
"password": {
"label": "Mật khẩu",
- "requirement": {
+ "suggestionsTitle": "",
+ "suggestion": {
"length": "Bao gồm ít nhất 8 kí tự",
"lowercase": "Bao gồm chữ thường",
"uppercase": "Bao gồm chữ in hoa",
@@ -1405,6 +1406,9 @@
"openNewTab": {
"label": ""
},
+ "withBorder": {
+ "label": ""
+ },
"items": {
"label": "",
"add": ""
diff --git a/packages/translation/src/lang/zh.json b/packages/translation/src/lang/zh.json
index 7d15715a2..0ed50e8eb 100644
--- a/packages/translation/src/lang/zh.json
+++ b/packages/translation/src/lang/zh.json
@@ -132,7 +132,8 @@
},
"password": {
"label": "密碼",
- "requirement": {
+ "suggestionsTitle": "",
+ "suggestion": {
"length": "至少包含 8 個字符",
"lowercase": "包含小寫字母",
"uppercase": "包含大寫字母",
@@ -1405,6 +1406,9 @@
"openNewTab": {
"label": "於新分頁開啟"
},
+ "withBorder": {
+ "label": ""
+ },
"items": {
"label": "書籤",
"add": "新增書籤"
diff --git a/packages/ui/src/components/index.tsx b/packages/ui/src/components/index.tsx
index 717359dac..9bc663a46 100644
--- a/packages/ui/src/components/index.tsx
+++ b/packages/ui/src/components/index.tsx
@@ -16,3 +16,9 @@ export { MaskedImage } from "./masked-image";
export { MaskedOrNormalImage } from "./masked-or-normal-image";
export { LanguageIcon } from "./language-icon";
export { Link } from "./link";
+export {
+ SelectGridLayout,
+ selectGridCardHeight,
+ selectGridCols,
+ selectGridScrollMaxHeight,
+} from "../select-grid-layout";
diff --git a/packages/ui/src/components/password-input/password-input.tsx b/packages/ui/src/components/password-input/password-input.tsx
index 6164ede02..22d4a75dc 100644
--- a/packages/ui/src/components/password-input/password-input.tsx
+++ b/packages/ui/src/components/password-input/password-input.tsx
@@ -20,15 +20,15 @@ export const CustomPasswordInput = ({ withPasswordRequirements, ...props }: Cust
};
const WithPasswordRequirements = (props: PasswordInputProps) => {
- const [value, setValue] = useState("");
+ const [password, setPassword] = useState("");
const onChange = (event: ChangeEvent) => {
- setValue(event.currentTarget.value);
+ setPassword(event.currentTarget.value);
props.onChange?.(event);
};
return (
-
+
);
diff --git a/packages/ui/src/components/password-input/password-requirement.tsx b/packages/ui/src/components/password-input/password-requirement.tsx
index 1e1eca197..99d694d31 100644
--- a/packages/ui/src/components/password-input/password-requirement.tsx
+++ b/packages/ui/src/components/password-input/password-requirement.tsx
@@ -1,14 +1,19 @@
import { rem, Text } from "@mantine/core";
-import { IconCheck, IconX } from "@tabler/icons-react";
+import { IconCheck, IconPoint } from "@tabler/icons-react";
+
+const getRequirementDisplay = (meets: boolean) =>
+ ({
+ true: { color: "teal", Icon: IconCheck },
+ false: { color: "dimmed", Icon: IconPoint },
+ })[String(meets) as "true" | "false"];
export function PasswordRequirement({ meets, label }: { meets: boolean; label: string }) {
+ const display = getRequirementDisplay(meets);
+ const Icon = display.Icon;
+
return (
-
- {meets ? (
-
- ) : (
-
- )}
+
+
{label}
diff --git a/packages/ui/src/components/password-input/password-requirements-popover.tsx b/packages/ui/src/components/password-input/password-requirements-popover.tsx
index 5d10611f9..33cb30e42 100644
--- a/packages/ui/src/components/password-input/password-requirements-popover.tsx
+++ b/packages/ui/src/components/password-input/password-requirements-popover.tsx
@@ -1,16 +1,24 @@
import type { PropsWithChildren } from "react";
import { useState } from "react";
-import { Popover, Progress } from "@mantine/core";
+import { Popover, Progress, Text } from "@mantine/core";
import { useScopedI18n } from "@homarr/translation/client";
import { passwordRequirements } from "@homarr/validation/user";
import { PasswordRequirement } from "./password-requirement";
+const strengthColorMap = [
+ { min: 100, color: "teal" },
+ { min: 51, color: "yellow" },
+ { min: 0, color: "dimmed" },
+] as const;
+
export const PasswordRequirementsPopover = ({ password, children }: PropsWithChildren<{ password: string }>) => {
+ const tPassword = useScopedI18n("user.field.password");
const requirements = useRequirements();
const strength = useStrength(password);
const [popoverOpened, setPopoverOpened] = useState(false);
+ const strengthColor = strengthColorMap.find(({ min }) => strength >= min)?.color ?? "dimmed";
const checks = (
<>
{requirements.map((requirement) => (
@@ -19,8 +27,6 @@ export const PasswordRequirementsPopover = ({ password, children }: PropsWithChi
>
);
- const color = strength === 100 ? "teal" : strength > 50 ? "yellow" : "red";
-
return (
@@ -29,7 +35,10 @@ export const PasswordRequirementsPopover = ({ password, children }: PropsWithChi
-
+
+
+ {tPassword("suggestionsTitle")}
+
{checks}
@@ -37,7 +46,7 @@ export const PasswordRequirementsPopover = ({ password, children }: PropsWithChi
};
const useRequirements = () => {
- const t = useScopedI18n("user.field.password.requirement");
+ const t = useScopedI18n("user.field.password.suggestion");
return passwordRequirements.map(({ check, value }) => ({ check, label: t(value) }));
};
diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts
index 364f7a394..1e8db7d58 100644
--- a/packages/ui/src/index.ts
+++ b/packages/ui/src/index.ts
@@ -3,6 +3,7 @@ import type { MantineProviderProps } from "@mantine/core";
import { theme } from "./theme";
export { theme } from "./theme";
+export { modalSizeForm, modalSizeSelect } from "./theme/modal";
export * from "./components";
export const uiConfiguration = {
diff --git a/packages/ui/src/select-grid-layout.tsx b/packages/ui/src/select-grid-layout.tsx
new file mode 100644
index 000000000..10f48f3df
--- /dev/null
+++ b/packages/ui/src/select-grid-layout.tsx
@@ -0,0 +1,50 @@
+"use client";
+
+import type { KeyboardEvent, ReactNode } from "react";
+import { Input, ScrollArea, SimpleGrid, Stack } from "@mantine/core";
+import { IconSearch } from "@tabler/icons-react";
+
+export const selectGridCols = { base: 2, xs: 3, sm: 4, md: 5 };
+export const selectGridCardHeight = 140;
+export const selectGridScrollMaxHeight = "70vh";
+
+interface SelectGridLayoutProps {
+ search: string;
+ onSearchChange: (value: string) => void;
+ placeholder: string;
+ onSearchKeyDown?: (event: KeyboardEvent) => void;
+ disableScroll?: boolean;
+ disableAutoFocus?: boolean;
+ children: ReactNode;
+}
+
+export const SelectGridLayout = ({
+ search,
+ onSearchChange,
+ placeholder,
+ onSearchKeyDown,
+ disableScroll = false,
+ disableAutoFocus = false,
+ children,
+}: SelectGridLayoutProps) => {
+ const grid = (
+
+ {children}
+
+ );
+
+ return (
+
+ onSearchChange(event.currentTarget.value)}
+ leftSection={}
+ placeholder={placeholder}
+ data-autofocus={disableAutoFocus ? undefined : true}
+ onKeyDown={onSearchKeyDown}
+ />
+
+ {disableScroll ? grid : {grid}}
+
+ );
+};
diff --git a/packages/ui/src/styles.css b/packages/ui/src/styles.css
index e4072e1d0..07a86f6a3 100644
--- a/packages/ui/src/styles.css
+++ b/packages/ui/src/styles.css
@@ -1,4 +1,5 @@
@import "@mantine/core/styles.css";
@import "@mantine/charts/styles.css";
@import "@mantine/dates/styles.css";
+@import "@gfazioli/mantine-onboarding-tour/styles.css";
@import "mantine-react-table/styles.css";
diff --git a/packages/ui/src/theme.ts b/packages/ui/src/theme.ts
index 9aea93dbf..2afa0f689 100644
--- a/packages/ui/src/theme.ts
+++ b/packages/ui/src/theme.ts
@@ -1,5 +1,7 @@
import { createTheme, rem } from "@mantine/core";
+import { modalComponent } from "./theme/modal";
+
export const theme = createTheme({
primaryColor: "red",
autoContrast: true,
@@ -99,5 +101,6 @@ export const theme = createTheme({
overlayProps: { radius: "sm", blur: 2 },
},
},
+ Modal: modalComponent,
},
});
diff --git a/packages/ui/src/theme/modal.ts b/packages/ui/src/theme/modal.ts
new file mode 100644
index 000000000..7bb9f4e69
--- /dev/null
+++ b/packages/ui/src/theme/modal.ts
@@ -0,0 +1,33 @@
+import type { MantineTheme, MantineThemeComponent, ModalProps } from "@mantine/core";
+
+export const modalSizeSelect = "xxl";
+export const modalSizeForm = "lg";
+
+const fixedModalSizes = new Set([modalSizeSelect, modalSizeForm]);
+
+const modalWidth = "min(var(--modal-size), calc(100vw - 2 * var(--mantine-spacing-md)))";
+
+export const modalComponent: MantineThemeComponent = {
+ vars: (_theme: MantineTheme, props: Partial) => {
+ if (props.size === modalSizeSelect) {
+ return { root: { "--modal-size": "75rem" } };
+ }
+ return { root: {} };
+ },
+ styles: (_theme: MantineTheme, props: Partial) => {
+ const isFixedSize = typeof props.size === "string" && fixedModalSizes.has(props.size);
+ if (!isFixedSize) {
+ return {};
+ }
+
+ return {
+ content: {
+ width: modalWidth,
+ minWidth: modalWidth,
+ },
+ body: {
+ width: "100%",
+ },
+ };
+ },
+};
diff --git a/packages/validation/src/user.ts b/packages/validation/src/user.ts
index 626ba2e8b..824567fa6 100644
--- a/packages/validation/src/user.ts
+++ b/packages/validation/src/user.ts
@@ -19,24 +19,10 @@ export const passwordRequirements = [
{ check: regexCheck(/[$&+,:;=?@#|'<>.^*()%!\-~`"_/\\[\]{}]/), value: "special" },
] satisfies {
check: (value: string) => boolean;
- value: keyof TranslationObject["user"]["field"]["password"]["requirement"];
+ value: keyof TranslationObject["user"]["field"]["password"]["suggestion"];
}[];
-export const userPasswordSchema = z
- .string()
- .min(8)
- .max(255)
- .refine(
- (value) => {
- return passwordRequirements.every((requirement) => requirement.check(value));
- },
- {
- params: createCustomErrorParams({
- key: "passwordRequirements",
- params: {},
- }),
- },
- );
+export const userPasswordSchema = z.string().min(8).max(255);
const addConfirmPasswordRefinement = <
TSchema extends z.ZodObject<{ password: z.core.$ZodString; confirmPassword: z.core.$ZodString }, z.core.$strip>,
diff --git a/packages/widgets/src/bookmarks/app-select-modal.tsx b/packages/widgets/src/bookmarks/app-select-modal.tsx
index b99751751..51b26aa8b 100644
--- a/packages/widgets/src/bookmarks/app-select-modal.tsx
+++ b/packages/widgets/src/bookmarks/app-select-modal.tsx
@@ -2,13 +2,13 @@
import { memo, useState } from "react";
import type { SelectProps } from "@mantine/core";
-import { Button, Group, Loader, Select, Stack } from "@mantine/core";
+import { Group, Loader, Select, Stack } from "@mantine/core";
import { IconCheck } from "@tabler/icons-react";
import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
import { useForm } from "@homarr/form";
-import { createModal } from "@homarr/modals";
+import { createModal, ModalFormFooter, modalSizeForm } from "@homarr/modals";
import { useI18n } from "@homarr/translation/client";
interface InnerProps {
@@ -47,6 +47,7 @@ export const AppSelectModal = createModal(({ actions, innerProps })
label={t("app.action.select.label")}
searchable
clearable
+ data-autofocus
leftSection={}
nothingFoundMessage={t("app.action.select.notFound")}
renderOption={renderSelectOption}
@@ -61,19 +62,13 @@ export const AppSelectModal = createModal(({ actions, innerProps })
})) ?? []
}
/>
-
-
-
-
+
);
}).withOptions({
defaultTitle: (t) => t("app.action.select.label"),
+ size: modalSizeForm,
});
const iconProps = {
diff --git a/packages/widgets/src/bookmarks/component.tsx b/packages/widgets/src/bookmarks/component.tsx
index 09066f90d..51fb6f781 100644
--- a/packages/widgets/src/bookmarks/component.tsx
+++ b/packages/widgets/src/bookmarks/component.tsx
@@ -16,7 +16,7 @@ export default function BookmarksWidget({ options, itemId }: WidgetComponentProp
const board = useRequiredBoard();
const [data] = clientApi.app.byIds.useSuspenseQuery(options.items, {
select(data) {
- return data.sort((appA, appB) => options.items.indexOf(appA.id) - options.items.indexOf(appB.id));
+ return data.toSorted((appA, appB) => options.items.indexOf(appA.id) - options.items.indexOf(appB.id));
},
});
@@ -56,6 +56,7 @@ export default function BookmarksWidget({ options, itemId }: WidgetComponentProp
hideIcon={options.hideIcon}
hideHostname={options.hideHostname}
openNewTab={options.openNewTab}
+ withBorder={options.withBorder}
hasIconColor={board.iconColor !== null}
/>
)}
@@ -67,6 +68,7 @@ export default function BookmarksWidget({ options, itemId }: WidgetComponentProp
hideIcon={options.hideIcon}
hideHostname={options.hideHostname}
openNewTab={options.openNewTab}
+ withBorder={options.withBorder}
hasIconColor={board.iconColor !== null}
/>
)}
@@ -81,6 +83,7 @@ interface FlexLayoutProps {
hideIcon: boolean;
hideHostname: boolean;
openNewTab: boolean;
+ withBorder: boolean;
hasIconColor: boolean;
}
@@ -91,6 +94,7 @@ const FlexLayout = ({
hideIcon,
hideHostname,
openNewTab,
+ withBorder,
hasIconColor,
}: FlexLayoutProps) => {
const board = useRequiredBoard();
@@ -106,7 +110,15 @@ const FlexLayout = ({
key={app.id}
w="100%"
>
-
+
{direction === "row" ? (
{
@@ -168,6 +182,7 @@ const GridLayout = ({
h="100%"
className={combineClasses(classes.card, classes["card-grid"])}
radius={board.itemRadius}
+ withBorder={withBorder}
p="xs"
>
{itemDirection === "horizontal" ? (
diff --git a/packages/widgets/src/bookmarks/index.tsx b/packages/widgets/src/bookmarks/index.tsx
index 24b3a618c..c57b909fd 100644
--- a/packages/widgets/src/bookmarks/index.tsx
+++ b/packages/widgets/src/bookmarks/index.tsx
@@ -24,6 +24,7 @@ export const { definition, componentLoader } = createWidgetDefinition("bookmarks
hideIcon: factory.switch({ defaultValue: false }),
hideHostname: factory.switch({ defaultValue: false }),
openNewTab: factory.switch({ defaultValue: true }),
+ withBorder: factory.switch({ defaultValue: false }),
items: factory.sortableItemList({
ItemComponent: ({ item, handle: Handle, removeItem, rootAttributes }) => {
return (
diff --git a/packages/widgets/src/docker/component.tsx b/packages/widgets/src/docker/component.tsx
index e47c77f33..5d869c52a 100644
--- a/packages/widgets/src/docker/component.tsx
+++ b/packages/widgets/src/docker/component.tsx
@@ -11,7 +11,7 @@ import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
import { humanFileSize, useTimeAgo } from "@homarr/common";
import type { ContainerState } from "@homarr/docker";
-import { containerStateColorMap } from "@homarr/docker/shared";
+import { containerStateColorMap, cpuUsageColor, memoryUsageColor, safeValue } from "@homarr/docker/shared";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useScopedI18n } from "@homarr/translation/client";
import { useTranslatedMantineReactTable } from "@homarr/ui/hooks";
@@ -28,25 +28,6 @@ const ContainerStateBadge = ({ state }: { state: ContainerState }) => {
);
};
-const memoryUsageColor = (number: number, state: string) => {
- const mbUsage = number / 1024 / 1024;
- if (mbUsage === 0 && state !== "running") return "red";
- if (mbUsage < 128) return "green";
- if (mbUsage < 256) return "yellow";
- if (mbUsage < 512) return "orange";
- return "red";
-};
-
-const cpuUsageColor = (number: number, state: string) => {
- if (number === 0 && state !== "running") return "red";
- if (number < 40) return "green";
- if (number < 60) return "yellow";
- if (number < 90) return "orange";
- return "red";
-};
-
-const safeValue = (value?: number, fallback = 0) => (value !== undefined && !isNaN(value) ? value : fallback);
-
const actionIconIconStyle: IconProps["style"] = {
height: "var(--ai-icon-size)",
width: "var(--ai-icon-size)",
diff --git a/packages/widgets/src/immich/server-stats/component.module.css b/packages/widgets/src/immich/server-stats/component.module.css
index 9154643ff..b16fdb597 100644
--- a/packages/widgets/src/immich/server-stats/component.module.css
+++ b/packages/widgets/src/immich/server-stats/component.module.css
@@ -1,20 +1,10 @@
.statItem {
padding: 12px;
border-radius: var(--mantine-radius-md);
- background-color: var(--mantine-color-gray-0);
+ background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
transition: all 0.2s ease;
&:hover {
- background-color: var(--mantine-color-gray-1);
- }
-}
-
-@media (prefers-color-scheme: dark) {
- .statItem {
- background-color: var(--mantine-color-dark-6);
-
- &:hover {
- background-color: var(--mantine-color-dark-5);
- }
+ background-color: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-5));
}
}
diff --git a/packages/widgets/src/index.tsx b/packages/widgets/src/index.tsx
index 572f70aa4..5e66efc4f 100644
--- a/packages/widgets/src/index.tsx
+++ b/packages/widgets/src/index.tsx
@@ -47,6 +47,7 @@ import * as systemResources from "./system-resources";
import * as timetable from "./timetable";
import * as tracearr from "./tracearr";
import * as umami from "./umami";
+import * as ups from "./ups";
import * as video from "./video";
import * as weather from "./weather";
@@ -93,6 +94,7 @@ export const widgetImports = {
tracearr,
speedtestTracker,
umami,
+ ups,
} satisfies WidgetImportRecord;
export type WidgetImports = typeof widgetImports;
diff --git a/packages/widgets/src/media-requests/list/component.tsx b/packages/widgets/src/media-requests/list/component.tsx
index 5d86c40a1..3a17c5e0f 100644
--- a/packages/widgets/src/media-requests/list/component.tsx
+++ b/packages/widgets/src/media-requests/list/component.tsx
@@ -1,17 +1,32 @@
"use client";
-import { ActionIcon, Anchor, Avatar, Badge, Card, Group, Image, ScrollArea, Stack, Text, Tooltip } from "@mantine/core";
-import { IconThumbDown, IconThumbUp } from "@tabler/icons-react";
+import {
+ ActionIcon,
+ Anchor,
+ Avatar,
+ Badge,
+ Box,
+ Card,
+ Group,
+ Image,
+ ScrollArea,
+ Stack,
+ Text,
+ Tooltip,
+} from "@mantine/core";
+import { IconSearch, IconThumbDown, IconThumbUp } from "@tabler/icons-react";
import type { RouterInputs, RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
import { useRequiredBoard } from "@homarr/boards/context";
import type { MediaRequestStatus } from "@homarr/integrations/types";
import { mediaAvailabilityConfiguration, mediaRequestStatusConfiguration } from "@homarr/integrations/types";
+import { openMediaRequestSearch } from "@homarr/spotlight";
import { useScopedI18n } from "@homarr/translation/client";
import type { WidgetComponentProps } from "../../definition";
import { NoIntegrationDataError } from "../../errors/no-data-integration";
+import classes from "../search-button.module.css";
export default function MediaServerWidget({
integrationIds,
@@ -43,7 +58,7 @@ export default function MediaServerWidget({
const newData = filteredData.concat(
data.requests.map((request) => ({ ...request, integrationId: data.integrationId })),
);
- return newData.sort((dataA, dataB) => {
+ return newData.toSorted((dataA, dataB) => {
if (dataA.status === dataB.status) {
return dataB.createdAt.getTime() - dataA.createdAt.getTime();
}
@@ -61,25 +76,46 @@ export default function MediaServerWidget({
if (mediaRequests.length === 0) throw new NoIntegrationDataError();
return (
-
-
- {mediaRequests.map((mediaRequest) => (
-
- ))}
-
-
+
+ {!isEditMode && }
+
+
+ {mediaRequests.map((mediaRequest) => (
+
+ ))}
+
+
+
);
}
+const MediaRequestSearchButton = ({ integrationIds }: { integrationIds: string[] }) => {
+ const t = useScopedI18n("search.mode.media");
+
+ return (
+
+ openMediaRequestSearch({ integrationIds })}
+ >
+
+
+
+ );
+};
+
interface MediaRequestCardProps {
request: RouterOutputs["widget"]["mediaRequests"]["getLatestRequests"][number];
isTiny: boolean;
diff --git a/packages/widgets/src/media-requests/search-button.module.css b/packages/widgets/src/media-requests/search-button.module.css
new file mode 100644
index 000000000..b131da24d
--- /dev/null
+++ b/packages/widgets/src/media-requests/search-button.module.css
@@ -0,0 +1,24 @@
+.searchRoot {
+ position: relative;
+ height: 100%;
+}
+
+.searchButton {
+ position: absolute;
+ top: var(--mantine-spacing-xs);
+ right: var(--mantine-spacing-xs);
+ z-index: 2;
+ opacity: 0;
+ transition: opacity 120ms ease;
+}
+
+.searchRoot:hover .searchButton,
+.searchRoot:focus-within .searchButton {
+ opacity: 1;
+}
+
+@media (hover: none) {
+ .searchButton {
+ opacity: 1;
+ }
+}
diff --git a/packages/widgets/src/media-requests/stats/component.tsx b/packages/widgets/src/media-requests/stats/component.tsx
index 4a1b6ed09..2960d7ae6 100644
--- a/packages/widgets/src/media-requests/stats/component.tsx
+++ b/packages/widgets/src/media-requests/stats/component.tsx
@@ -1,6 +1,6 @@
"use client";
-import { Avatar, Card, Grid, Group, Stack, Text, Tooltip } from "@mantine/core";
+import { ActionIcon, Avatar, Box, Card, Grid, Group, Stack, Text, Tooltip } from "@mantine/core";
import type { Icon } from "@tabler/icons-react";
import {
IconDeviceTv,
@@ -9,6 +9,7 @@ import {
IconMovie,
IconPlayerPlay,
IconReceipt,
+ IconSearch,
IconThumbDown,
IconThumbUp,
} from "@tabler/icons-react";
@@ -17,11 +18,13 @@ import combineClasses from "clsx";
import { clientApi } from "@homarr/api/client";
import { useRequiredBoard } from "@homarr/boards/context";
import type { RequestStats } from "@homarr/integrations/types";
+import { openMediaRequestSearch } from "@homarr/spotlight";
import { useScopedI18n } from "@homarr/translation/client";
import type { WidgetComponentProps } from "../../definition";
import { NoIntegrationDataError } from "../../errors/no-data-integration";
import classes from "./component.module.css";
+import searchClasses from "../search-button.module.css";
const OVERSEERR_COLOR = "#ECB000";
const JELLYSEERR_COLOR = "#6677CC";
@@ -93,83 +96,104 @@ export default function MediaServerWidget({
const isTiny = width < 256;
return (
-
-
-
- {t("titles.stats.main")}
-
-
- {data.map((stat) => (
-
-
-
-
-
-
- {stat.number}
+
+ {!isEditMode && }
+
+
+
+ {t("titles.stats.main")}
+
+
+ {data.map((stat) => (
+
+
+
+
+
+
+ {stat.number}
+
+
+
+
+
+ ))}
+
+
+
+
+ {t("titles.users.main")} ({t("titles.users.requests")})
+
+
+ {requestStats.users.slice(0, 10).map((user) => (
+
+
+
+
+
+
+
+ {user.displayName}
-
-
-
- ))}
-
-
-
-
- {t("titles.users.main")} ({t("titles.users.requests")})
-
-
- {requestStats.users.slice(0, 10).map((user) => (
-
-
-
-
-
-
-
- {user.displayName}
+
+
+ {user.requestCount}
-
-
- {user.requestCount}
-
-
-
- ))}
+
+ ))}
+
-
+
);
}
+
+const MediaRequestSearchButton = ({ integrationIds }: { integrationIds: string[] }) => {
+ const t = useScopedI18n("search.mode.media");
+
+ return (
+
+ openMediaRequestSearch({ integrationIds })}
+ >
+
+
+
+ );
+};
diff --git a/packages/widgets/src/modals/embedded-app-edit-form.tsx b/packages/widgets/src/modals/embedded-app-edit-form.tsx
new file mode 100644
index 000000000..81b336869
--- /dev/null
+++ b/packages/widgets/src/modals/embedded-app-edit-form.tsx
@@ -0,0 +1,95 @@
+"use client";
+
+import { useImperativeHandle, useRef } from "react";
+import { Alert, Button, Center, Loader, Stack, Text } from "@mantine/core";
+import { IconInfoCircle } from "@tabler/icons-react";
+
+import { clientApi } from "@homarr/api/client";
+import type { AppFormHandle } from "@homarr/forms-collection";
+import { AppForm } from "@homarr/forms-collection";
+import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
+import { useI18n } from "@homarr/translation/client";
+
+export interface EmbeddedAppEditFormHandle {
+ submitIfDirty: () => Promise;
+}
+
+interface EmbeddedAppEditFormProps {
+ appId: string;
+ handleRef: React.Ref;
+}
+
+export const EmbeddedAppEditForm = ({ appId, handleRef }: EmbeddedAppEditFormProps) => {
+ const t = useI18n();
+ const utils = clientApi.useUtils();
+ const appFormRef = useRef(null);
+ const { data: app, isPending: isLoadingApp, isError, refetch } = clientApi.app.byId.useQuery({ id: appId });
+
+ const { mutateAsync, isPending: isMutating } = clientApi.app.update.useMutation({
+ onSuccess: async () => {
+ await utils.app.invalidate();
+ showSuccessNotification({
+ title: t("app.page.edit.notification.success.title"),
+ message: t("app.page.edit.notification.success.message"),
+ });
+ },
+ onError: () => {
+ showErrorNotification({
+ title: t("app.page.edit.notification.error.title"),
+ message: t("app.page.edit.notification.error.message"),
+ });
+ },
+ });
+
+ useImperativeHandle(
+ handleRef,
+ () => ({
+ submitIfDirty: async () => {
+ if (!appFormRef.current?.isDirty()) {
+ return true;
+ }
+
+ return appFormRef.current.submit();
+ },
+ }),
+ [],
+ );
+
+ if (isLoadingApp) {
+ return (
+
+
+
+ );
+ }
+
+ if (isError || !app) {
+ return (
+
+ {t("app.action.select.notFound")}
+
+
+ );
+ }
+
+ return (
+
+ } color="blue" variant="light">
+ {t("item.edit.app.propagationNotice")}
+
+ {
+ await mutateAsync({ id: appId, ...values });
+ }}
+ isPending={isMutating}
+ />
+
+ );
+};
diff --git a/packages/widgets/src/modals/widget-advanced-options-modal.tsx b/packages/widgets/src/modals/widget-advanced-options-modal.tsx
index dc785d186..5a1021bd2 100644
--- a/packages/widgets/src/modals/widget-advanced-options-modal.tsx
+++ b/packages/widgets/src/modals/widget-advanced-options-modal.tsx
@@ -1,9 +1,9 @@
"use client";
-import { Button, CloseButton, ColorInput, Group, Input, Stack, TextInput, useMantineTheme } from "@mantine/core";
+import { CloseButton, ColorInput, Input, Stack, TextInput, useMantineTheme } from "@mantine/core";
import { useForm } from "@homarr/form";
-import { createModal } from "@homarr/modals";
+import { createModal, ModalFormFooter, modalSizeForm } from "@homarr/modals";
import { useI18n } from "@homarr/translation/client";
import { TextMultiSelect } from "@homarr/ui";
import type { BoardItemAdvancedOptions } from "@homarr/validation/shared";
@@ -34,6 +34,7 @@ export const WidgetAdvancedOptionsModal = createModal(({ actions, in
form.setFieldValue("title", "")} />}
/>
@@ -53,12 +54,7 @@ export const WidgetAdvancedOptionsModal = createModal(({ actions, in
}
{...form.getInputProps("borderColor")}
/>
-
-
-
-
+
);
@@ -66,7 +62,7 @@ export const WidgetAdvancedOptionsModal = createModal(({ actions, in
defaultTitle(t) {
return t("item.edit.advancedOptions.title");
},
- size: "lg",
+ size: modalSizeForm,
transitionProps: {
duration: 0,
},
diff --git a/packages/widgets/src/modals/widget-edit-modal.tsx b/packages/widgets/src/modals/widget-edit-modal.tsx
index 1722bcaf9..1c2f39700 100644
--- a/packages/widgets/src/modals/widget-edit-modal.tsx
+++ b/packages/widgets/src/modals/widget-edit-modal.tsx
@@ -1,13 +1,15 @@
"use client";
-import { useState } from "react";
-import { Button, Group, Stack } from "@mantine/core";
+import { useRef, useState } from "react";
+import type { FormEvent } from "react";
+import { Button, Group, Stack, Tabs } from "@mantine/core";
import { schemaResolver } from "@mantine/form";
import { z } from "zod/v4";
import { objectEntries } from "@homarr/common";
+import { useSession } from "@homarr/auth/client";
import type { WidgetKind } from "@homarr/definitions";
-import { createModal, useModalAction } from "@homarr/modals";
+import { createModal, ModalFormFooter, modalSizeForm, useModalAction } from "@homarr/modals";
import type { SettingsContextProps } from "@homarr/settings/creator";
import { useI18n } from "@homarr/translation/client";
import { zodErrorMap } from "@homarr/validation/form/i18n";
@@ -20,6 +22,8 @@ import type { OptionsBuilderResult } from "../options";
import type { IntegrationSelectOption } from "../widget-integration-select";
import { WidgetIntegrationSelect } from "../widget-integration-select";
import { WidgetAdvancedOptionsModal } from "./widget-advanced-options-modal";
+import type { EmbeddedAppEditFormHandle } from "./embedded-app-edit-form";
+import { EmbeddedAppEditForm } from "./embedded-app-edit-form";
export interface WidgetEditModalState {
options: Record;
@@ -34,13 +38,16 @@ interface ModalProps {
integrationData: IntegrationSelectOption[];
integrationSupport: boolean;
settings: SettingsContextProps;
+ appId?: string;
}
export const WidgetEditModal = createModal>(({ actions, innerProps }) => {
const t = useI18n();
+ const { data: session } = useSession();
const [advancedOptions, setAdvancedOptions] = useState(innerProps.value.advancedOptions);
+ const appEditRef = useRef(null);
+ const [isSubmitting, setIsSubmitting] = useState(false);
- // Translate the error messages
z.config({
customError: zodErrorMap(t),
});
@@ -77,55 +84,93 @@ export const WidgetEditModal = createModal>(({ actions, i
});
const { openModal } = useModalAction(WidgetAdvancedOptionsModal);
- return (
-
);
@@ -157,5 +229,5 @@ export const WidgetEditModal = createModal>(({ actions, i
defaultTitle(t) {
return t("item.edit.title");
},
- size: "lg",
+ size: modalSizeForm,
});
diff --git a/packages/widgets/src/ups/component.tsx b/packages/widgets/src/ups/component.tsx
new file mode 100644
index 000000000..2b98a1437
--- /dev/null
+++ b/packages/widgets/src/ups/component.tsx
@@ -0,0 +1,256 @@
+"use client";
+
+import { Badge, Box, Card, Group, Progress, RingProgress, ScrollArea, Stack, Text } from "@mantine/core";
+
+import { clientApi } from "@homarr/api/client";
+import { formatDuration } from "@homarr/common";
+import type { UpsStatus, UpsSummary } from "@homarr/integrations/types";
+import type { ScopedTranslationFunction } from "@homarr/translation";
+import { useScopedI18n } from "@homarr/translation/client";
+
+import type { WidgetComponentProps } from "../definition";
+import { NoIntegrationDataError } from "../errors/no-data-integration";
+import { NoIntegrationSelectedError } from "../errors/no-integration-selected";
+
+const statusColors: Record = {
+ online: "green",
+ charging: "blue",
+ onBattery: "yellow",
+ lowBattery: "red",
+ unknown: "gray",
+};
+
+type UpsLayout = "mini" | "compact" | "full";
+
+export default function UpsWidget({ options, integrationIds, isEditMode, width }: WidgetComponentProps<"ups">) {
+ if (integrationIds.length === 0) {
+ throw new NoIntegrationSelectedError();
+ }
+
+ return ;
+}
+
+interface UpsContentProps {
+ integrationIds: string[];
+ options: WidgetComponentProps<"ups">["options"];
+ isEditMode: boolean;
+ width: number;
+}
+
+function UpsContent({ integrationIds, options, isEditMode, width }: UpsContentProps) {
+ const t = useScopedI18n("widget.ups");
+ const [data] = clientApi.widget.ups.getSummaries.useSuspenseQuery({ integrationIds });
+
+ const utils = clientApi.useUtils();
+ clientApi.widget.ups.subscribeSummaries.useSubscription(
+ { integrationIds },
+ {
+ enabled: !isEditMode,
+ onData(newData) {
+ utils.widget.ups.getSummaries.setData({ integrationIds }, (prevData) => {
+ if (!prevData) return prevData;
+ return prevData.map((instance) =>
+ instance.integrationId === newData.integrationId
+ ? { ...instance, summaries: newData.summaries, updatedAt: newData.timestamp }
+ : instance,
+ );
+ });
+ },
+ },
+ );
+
+ const devices = data.flatMap((instance) =>
+ instance.summaries.map((summary) => ({ key: `${instance.integrationId}:${summary.id}`, summary })),
+ );
+
+ if (devices.length === 0) {
+ throw new NoIntegrationDataError();
+ }
+
+ // Pick a layout from the available width so the widget stays useful at any size. The surrounding
+ // ScrollArea keeps every device reachable at any height (e.g. a tall, narrow 1x3 widget scrolls).
+ // - full: ring beside a column of stats (runtime, load, voltage)
+ // - compact (~2 columns): ring beside the name and a status badge
+ // - mini (~1 column): a text badge no longer fits, so status becomes a colour dot next to the name
+ const layout: UpsLayout = width < 150 ? "mini" : width < 256 ? "compact" : "full";
+
+ return (
+
+
+ {devices.map(({ key, summary }) => (
+
+ ))}
+
+
+ );
+}
+
+interface UpsDeviceCardProps {
+ summary: UpsSummary;
+ options: WidgetComponentProps<"ups">["options"];
+ layout: UpsLayout;
+ t: ScopedTranslationFunction<"widget.ups">;
+}
+
+function UpsDeviceCard({ summary, options, layout, t }: UpsDeviceCardProps) {
+ const showRing = options.showBattery && summary.batteryCharge !== null;
+ const statusBadge = (
+
+ {t(`status.${summary.status}`)}
+
+ );
+
+ if (layout === "mini") {
+ return (
+
+
+
+
+ {summary.name}
+
+
+ {showRing && summary.batteryCharge !== null && (
+
+
+ {Math.round(summary.batteryCharge)}%
+
+ }
+ />
+
+ )}
+
+ );
+ }
+
+ if (layout === "compact") {
+ return (
+
+
+ {showRing && summary.batteryCharge !== null && (
+
+ {Math.round(summary.batteryCharge)}%
+
+ }
+ />
+ )}
+
+
+ {summary.name}
+
+ {statusBadge}
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ {summary.name}
+
+ {statusBadge}
+
+
+
+ {showRing && summary.batteryCharge !== null && (
+
+ {Math.round(summary.batteryCharge)}%
+
+ }
+ />
+ )}
+
+
+ {options.showBattery && summary.batteryRuntime !== null && (
+
+ )}
+
+ {options.showLoad && summary.load !== null && (
+
+
+
+ {t("field.load")}
+
+ {Math.round(summary.load)}%
+
+
+
+ )}
+
+ {options.showVoltage && (summary.inputVoltage !== null || summary.outputVoltage !== null) && (
+
+ {summary.inputVoltage !== null && (
+
+ )}
+ {summary.outputVoltage !== null && (
+
+ )}
+
+ )}
+
+
+
+ );
+}
+
+function StatRow({ label, value }: { label: string; value: string }) {
+ return (
+
+
+ {label}
+
+
+ {value}
+
+
+ );
+}
+
+function clampPercent(value: number): number {
+ return Math.min(100, Math.max(0, value));
+}
+
+function getChargeColor(charge: number): string {
+ if (charge > 50) return "green";
+ if (charge > 20) return "yellow";
+ return "red";
+}
+
+function getLoadColor(load: number): string {
+ if (load > 90) return "red";
+ if (load > 70) return "yellow";
+ return "blue";
+}
diff --git a/packages/widgets/src/ups/index.ts b/packages/widgets/src/ups/index.ts
new file mode 100644
index 000000000..681daf417
--- /dev/null
+++ b/packages/widgets/src/ups/index.ts
@@ -0,0 +1,30 @@
+import { IconBatteryCharging, IconBatteryOff } from "@tabler/icons-react";
+
+import { getIntegrationKindsByCategory } from "@homarr/definitions";
+
+import { createWidgetDefinition } from "../definition";
+import { optionsBuilder } from "../options";
+
+export const { definition, componentLoader } = createWidgetDefinition("ups", {
+ icon: IconBatteryCharging,
+ createOptions() {
+ return optionsBuilder.from((factory) => ({
+ showBattery: factory.switch({
+ defaultValue: true,
+ }),
+ showLoad: factory.switch({
+ defaultValue: true,
+ }),
+ showVoltage: factory.switch({
+ defaultValue: true,
+ }),
+ }));
+ },
+ supportedIntegrations: getIntegrationKindsByCategory("ups"),
+ errors: {
+ INTERNAL_SERVER_ERROR: {
+ icon: IconBatteryOff,
+ message: (t) => t("widget.ups.error.internalServerError"),
+ },
+ },
+}).withDynamicImport(() => import("./component"));
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index b26cef0eb..35770c256 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -16,8 +16,8 @@ catalogs:
specifier: ^8.0.0
version: 8.0.0
'@ctrl/qbittorrent':
- specifier: ^9.12.1
- version: 9.12.1
+ specifier: ^9.12.2
+ version: 9.12.2
'@ctrl/transmission':
specifier: ^7.4.0
version: 7.4.0
@@ -39,6 +39,9 @@ catalogs:
'@extractus/feed-extractor':
specifier: 7.2.1
version: 7.2.1
+ '@gfazioli/mantine-onboarding-tour':
+ specifier: ^4.0.1
+ version: 4.0.2
'@gitbeaker/rest':
specifier: ^43.8.0
version: 43.8.0
@@ -58,38 +61,38 @@ catalogs:
specifier: ^1.4.0
version: 1.4.0
'@mantine/charts':
- specifier: ^9.2.1
- version: 9.2.1
+ specifier: ^9.2.2
+ version: 9.2.2
'@mantine/colors-generator':
- specifier: ^9.2.1
- version: 9.2.1
+ specifier: ^9.2.2
+ version: 9.2.2
'@mantine/core':
- specifier: ^9.2.1
- version: 9.2.1
+ specifier: ^9.2.2
+ version: 9.2.2
'@mantine/dates':
- specifier: ^9.2.1
- version: 9.2.1
+ specifier: ^9.2.2
+ version: 9.2.2
'@mantine/dropzone':
- specifier: ^9.2.1
- version: 9.2.1
+ specifier: ^9.2.2
+ version: 9.2.2
'@mantine/form':
- specifier: ^9.2.1
- version: 9.2.1
+ specifier: ^9.2.2
+ version: 9.2.2
'@mantine/hooks':
- specifier: ^9.2.1
- version: 9.2.1
+ specifier: ^9.2.2
+ version: 9.2.2
'@mantine/modals':
- specifier: ^9.2.1
- version: 9.2.1
+ specifier: ^9.2.2
+ version: 9.2.2
'@mantine/notifications':
- specifier: ^9.2.1
- version: 9.2.1
+ specifier: ^9.2.2
+ version: 9.2.2
'@mantine/spotlight':
- specifier: ^9.2.1
- version: 9.2.1
+ specifier: ^9.2.2
+ version: 9.2.2
'@mantine/tiptap':
- specifier: ^9.2.1
- version: 9.2.1
+ specifier: ^9.2.2
+ version: 9.2.2
'@octokit/auth-app':
specifier: ^8.2.0
version: 8.2.0
@@ -124,71 +127,71 @@ catalogs:
specifier: ^3.44.0
version: 3.44.0
'@tanstack/react-query':
- specifier: ^5.100.13
+ specifier: ^5.100.14
version: 5.100.14
'@tanstack/react-query-devtools':
- specifier: ^5.100.13
+ specifier: ^5.100.14
version: 5.100.14
'@tanstack/react-query-next-experimental':
- specifier: ^5.100.13
+ specifier: ^5.100.14
version: 5.100.14
'@testcontainers/mysql':
- specifier: ^12.0.0
- version: 12.0.0
+ specifier: ^12.0.1
+ version: 12.0.1
'@testcontainers/postgresql':
- specifier: ^12.0.0
- version: 12.0.0
+ specifier: ^12.0.1
+ version: 12.0.1
'@testcontainers/redis':
- specifier: ^12.0.0
- version: 12.0.0
+ specifier: ^12.0.1
+ version: 12.0.1
'@tiptap/extension-color':
- specifier: 3.23.6
- version: 3.23.6
+ specifier: 3.24.0
+ version: 3.24.0
'@tiptap/extension-highlight':
- specifier: 3.23.6
- version: 3.23.6
+ specifier: 3.24.0
+ version: 3.24.0
'@tiptap/extension-image':
- specifier: 3.23.6
- version: 3.23.6
+ specifier: 3.24.0
+ version: 3.24.0
'@tiptap/extension-link':
- specifier: ^3.23.6
- version: 3.23.6
+ specifier: ^3.24.0
+ version: 3.24.0
'@tiptap/extension-placeholder':
- specifier: ^3.23.6
- version: 3.23.6
+ specifier: ^3.24.0
+ version: 3.24.0
'@tiptap/extension-table':
- specifier: 3.23.6
- version: 3.23.6
+ specifier: 3.24.0
+ version: 3.24.0
'@tiptap/extension-table-cell':
- specifier: 3.23.6
- version: 3.23.6
+ specifier: 3.24.0
+ version: 3.24.0
'@tiptap/extension-table-header':
- specifier: 3.23.6
- version: 3.23.6
+ specifier: 3.24.0
+ version: 3.24.0
'@tiptap/extension-table-row':
- specifier: 3.23.6
- version: 3.23.6
+ specifier: 3.24.0
+ version: 3.24.0
'@tiptap/extension-task-item':
- specifier: 3.23.6
- version: 3.23.6
+ specifier: 3.24.0
+ version: 3.24.0
'@tiptap/extension-task-list':
- specifier: 3.23.6
- version: 3.23.6
+ specifier: 3.24.0
+ version: 3.24.0
'@tiptap/extension-text-align':
- specifier: 3.23.6
- version: 3.23.6
+ specifier: 3.24.0
+ version: 3.24.0
'@tiptap/extension-text-style':
- specifier: 3.23.6
- version: 3.23.6
+ specifier: 3.24.0
+ version: 3.24.0
'@tiptap/extension-underline':
- specifier: 3.23.6
- version: 3.23.6
+ specifier: 3.24.0
+ version: 3.24.0
'@tiptap/react':
- specifier: ^3.23.6
- version: 3.23.6
+ specifier: ^3.24.0
+ version: 3.24.0
'@tiptap/starter-kit':
- specifier: ^3.23.6
- version: 3.23.6
+ specifier: ^3.24.0
+ version: 3.24.0
'@trpc/client':
specifier: ^11.17.0
version: 11.17.0
@@ -205,8 +208,8 @@ catalogs:
specifier: ^11.17.0
version: 11.17.0
'@turbo/gen':
- specifier: ^2.9.14
- version: 2.9.14
+ specifier: ^2.9.16
+ version: 2.9.16
'@types/adm-zip':
specifier: 0.5.8
version: 0.5.8
@@ -322,8 +325,8 @@ catalogs:
specifier: ^4.2.0
version: 4.2.0
dayjs:
- specifier: ^1.11.20
- version: 1.11.20
+ specifier: ^1.11.21
+ version: 1.11.21
deepmerge:
specifier: 4.3.1
version: 4.3.1
@@ -367,11 +370,11 @@ catalogs:
specifier: ^2.2.1
version: 2.2.1
ioredis:
- specifier: 5.10.1
- version: 5.10.1
+ specifier: 5.11.0
+ version: 5.11.0
isomorphic-dompurify:
- specifier: ^3.14.0
- version: 3.14.0
+ specifier: ^3.15.0
+ version: 3.15.0
jotai:
specifier: ^2.20.0
version: 2.20.0
@@ -388,14 +391,14 @@ catalogs:
specifier: ^4.5.0
version: 4.5.0
mantine-react-table:
- specifier: 2.0.0-beta.9
- version: 2.0.0-beta.9
+ specifier: npm:mantine-react-table-open@9.0.3
+ version: 9.0.3
maria2:
specifier: ^0.4.1
version: 0.4.1
mysql2:
- specifier: 3.22.3
- version: 3.22.3
+ specifier: 3.22.4
+ version: 3.22.4
next:
specifier: 16.2.6
version: 16.2.6
@@ -403,8 +406,8 @@ catalogs:
specifier: 5.0.0-beta.31
version: 5.0.0-beta.31
next-intl:
- specifier: 4.12.0
- version: 4.12.0
+ specifier: 4.13.0
+ version: 4.13.0
node-loader:
specifier: ^2.1.0
version: 2.1.0
@@ -412,11 +415,11 @@ catalogs:
specifier: ^5.0.5
version: 5.0.5
oxfmt:
- specifier: ^0.51.0
- version: 0.51.0
+ specifier: ^0.52.0
+ version: 0.52.0
oxlint:
- specifier: ^1.66.0
- version: 1.66.0
+ specifier: ^1.67.0
+ version: 1.67.0
pg:
specifier: ^8.21.0
version: 8.21.0
@@ -436,8 +439,8 @@ catalogs:
specifier: 19.2.6
version: 19.2.6
react-error-boundary:
- specifier: ^6.1.1
- version: 6.1.1
+ specifier: ^6.1.2
+ version: 6.1.2
react-markdown:
specifier: ^10.1.0
version: 10.1.0
@@ -469,8 +472,8 @@ catalogs:
specifier: ^5.32.6
version: 5.32.6
testcontainers:
- specifier: ^12.0.0
- version: 12.0.0
+ specifier: ^12.0.1
+ version: 12.0.1
timezones-ical-library:
specifier: ^2.2.0
version: 2.2.0
@@ -484,8 +487,8 @@ catalogs:
specifier: 4.20.4
version: 4.20.4
turbo:
- specifier: ^2.9.14
- version: 2.9.14
+ specifier: ^2.9.16
+ version: 2.9.16
typescript:
specifier: ^5.9.3
version: 5.9.3
@@ -527,14 +530,14 @@ catalogs:
version: 5.0.0
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'
@@ -544,7 +547,7 @@ overrides:
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:
@@ -582,10 +585,10 @@ importers:
version: 14.1.1(semantic-release@25.0.3(typescript@5.9.3))
'@testcontainers/redis':
specifier: 'catalog:'
- version: 12.0.0
+ version: 12.0.1
'@turbo/gen':
specifier: 'catalog:'
- version: 2.9.14(@types/node@24.12.4)
+ version: 2.9.16(@types/node@24.12.4)
'@vitejs/plugin-react':
specifier: 'catalog:'
version: 5.2.0(vite@7.1.12(@types/node@24.12.4)(jiti@1.21.7)(sass@1.100.0)(sugarss@5.0.0(postcss@8.5.8))(terser@5.46.1)(tsx@4.21.0)(yaml@2.9.0))
@@ -609,19 +612,19 @@ importers:
version: 2.2.3
oxfmt:
specifier: 'catalog:'
- version: 0.51.0
+ version: 0.52.0
oxlint:
specifier: 'catalog:'
- version: 1.66.0
+ version: 1.67.0
semantic-release:
specifier: 'catalog:'
version: 25.0.3(typescript@5.9.3)
testcontainers:
specifier: 'catalog:'
- version: 12.0.0
+ version: 12.0.1
turbo:
specifier: 'catalog:'
- version: 2.9.14
+ version: 2.9.16
typescript:
specifier: 'catalog:'
version: 5.9.3
@@ -646,6 +649,9 @@ importers:
'@dnd-kit/utilities':
specifier: 'catalog:'
version: 3.2.2(react@19.2.6)
+ '@gfazioli/mantine-onboarding-tour':
+ specifier: 'catalog:'
+ version: 4.0.2(@mantine/core@9.2.2(@mantine/hooks@9.2.2(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.2.2(react@19.2.6))(@tabler/icons-react@3.44.0(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@homarr/analytics':
specifier: workspace:^0.1.0
version: link:../../packages/analytics
@@ -741,22 +747,22 @@ importers:
version: link:../../packages/widgets
'@mantine/colors-generator':
specifier: 'catalog:'
- version: 9.2.1(chroma-js@3.2.0)
+ version: 9.2.2(chroma-js@3.2.0)
'@mantine/core':
specifier: 'catalog:'
- version: 9.2.1(@mantine/hooks@9.2.1(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+ version: 9.2.2(@mantine/hooks@9.2.2(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@mantine/dropzone':
specifier: 'catalog:'
- version: 9.2.1(@mantine/core@9.2.1(@mantine/hooks@9.2.1(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.2.1(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+ version: 9.2.2(@mantine/core@9.2.2(@mantine/hooks@9.2.2(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.2.2(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@mantine/hooks':
specifier: 'catalog:'
- version: 9.2.1(react@19.2.6)
+ version: 9.2.2(react@19.2.6)
'@mantine/modals':
specifier: 'catalog:'
- version: 9.2.1(@mantine/core@9.2.1(@mantine/hooks@9.2.1(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.2.1(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+ version: 9.2.2(@mantine/core@9.2.2(@mantine/hooks@9.2.2(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.2.2(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@mantine/tiptap':
specifier: 'catalog:'
- version: 9.2.1(@mantine/core@9.2.1(@mantine/hooks@9.2.1(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.2.1(react@19.2.6))(@tiptap/extension-link@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6))(@tiptap/react@3.23.6(@floating-ui/dom@1.7.6)(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+ version: 9.2.2(@mantine/core@9.2.2(@mantine/hooks@9.2.2(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.2.2(react@19.2.6))(@tiptap/extension-link@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0))(@tiptap/react@3.24.0(@floating-ui/dom@1.7.6)(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@tabler/icons-react':
specifier: 'catalog:'
version: 3.44.0(react@19.2.6)
@@ -801,7 +807,7 @@ importers:
version: 2.1.1
dayjs:
specifier: 'catalog:'
- version: 1.11.20
+ version: 1.11.21
dotenv:
specifier: 'catalog:'
version: 17.4.2
@@ -813,13 +819,13 @@ importers:
version: 13.0.6
isomorphic-dompurify:
specifier: 'catalog:'
- version: 3.14.0(@noble/hashes@2.0.1)
+ version: 3.15.0(@noble/hashes@2.0.1)
jotai:
specifier: 'catalog:'
version: 2.20.0(@babel/core@7.29.0)(@babel/template@7.28.6)(@types/react@19.2.15)(react@19.2.6)
mantine-react-table:
specifier: 'catalog:'
- version: 2.0.0-beta.9(@mantine/core@9.2.1(@mantine/hooks@9.2.1(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/dates@9.2.1(@mantine/core@9.2.1(@mantine/hooks@9.2.1(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.2.1(react@19.2.6))(dayjs@1.11.20)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.2.1(react@19.2.6))(@tabler/icons-react@3.44.0(react@19.2.6))(clsx@2.1.1)(dayjs@1.11.20)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+ version: mantine-react-table-open@9.0.3(@mantine/core@9.2.2(@mantine/hooks@9.2.2(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/dates@9.2.2(@mantine/core@9.2.2(@mantine/hooks@9.2.2(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.2.2(react@19.2.6))(dayjs@1.11.21)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.2.2(react@19.2.6))(@tabler/icons-react@3.44.0(react@19.2.6))(clsx@2.1.1)(dayjs@1.11.21)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
next:
specifier: 'catalog:'
version: 16.2.6(@babel/core@7.29.0)(@playwright/test@1.60.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.100.0)
@@ -837,7 +843,7 @@ importers:
version: 19.2.6(react@19.2.6)
react-error-boundary:
specifier: 'catalog:'
- version: 6.1.1(react@19.2.6)
+ version: 6.1.2(react@19.2.6)
react-simple-code-editor:
specifier: 'catalog:'
version: 0.14.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
@@ -946,7 +952,7 @@ importers:
version: 10.0.1
dayjs:
specifier: 'catalog:'
- version: 1.11.20
+ version: 1.11.21
dotenv:
specifier: 'catalog:'
version: 17.4.2
@@ -1289,7 +1295,7 @@ importers:
version: 1.1.1
dayjs:
specifier: 'catalog:'
- version: 1.11.20
+ version: 1.11.21
next:
specifier: 'catalog:'
version: 16.2.6(@babel/core@7.29.0)(@playwright/test@1.60.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.100.0)
@@ -1332,13 +1338,13 @@ importers:
version: 0.2.9
drizzle-orm:
specifier: 'catalog:'
- version: 0.45.2(@libsql/client-wasm@0.14.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.10.0)(gel@2.0.0)(mysql2@3.22.3(@types/node@24.12.4))(pg@8.21.0)
+ version: 0.45.2(@libsql/client-wasm@0.14.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.10.0)(gel@2.0.0)(mysql2@3.22.4(@types/node@24.12.4))(pg@8.21.0)
ioredis:
specifier: 'catalog:'
- version: 5.10.1
+ version: 5.11.0
mysql2:
specifier: 'catalog:'
- version: 3.22.3(@types/node@24.12.4)
+ version: 3.22.4(@types/node@24.12.4)
pg:
specifier: 'catalog:'
version: 8.21.0
@@ -1483,16 +1489,16 @@ importers:
version: link:../server-settings
'@mantine/core':
specifier: 'catalog:'
- version: 9.2.1(@mantine/hooks@9.2.1(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+ version: 9.2.2(@mantine/hooks@9.2.2(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@paralleldrive/cuid2':
specifier: 'catalog:'
version: 3.3.0
'@testcontainers/mysql':
specifier: 'catalog:'
- version: 12.0.0
+ version: 12.0.1
'@testcontainers/postgresql':
specifier: 'catalog:'
- version: 12.0.0
+ version: 12.0.1
better-sqlite3:
specifier: 'catalog:'
version: 12.10.0
@@ -1504,13 +1510,13 @@ importers:
version: 0.31.10
drizzle-orm:
specifier: 'catalog:'
- version: 0.45.2(@libsql/client-wasm@0.14.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.10.0)(gel@2.0.0)(mysql2@3.22.3(@types/node@24.12.4))(pg@8.21.0)
+ version: 0.45.2(@libsql/client-wasm@0.14.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.10.0)(gel@2.0.0)(mysql2@3.22.4(@types/node@24.12.4))(pg@8.21.0)
drizzle-zod:
specifier: 'catalog:'
- version: 0.8.3(drizzle-orm@0.45.2(@libsql/client-wasm@0.14.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.10.0)(gel@2.0.0)(mysql2@3.22.3(@types/node@24.12.4))(pg@8.21.0))(zod@4.4.3)
+ version: 0.8.3(drizzle-orm@0.45.2(@libsql/client-wasm@0.14.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.10.0)(gel@2.0.0)(mysql2@3.22.4(@types/node@24.12.4))(pg@8.21.0))(zod@4.4.3)
mysql2:
specifier: 'catalog:'
- version: 3.22.3(@types/node@24.12.4)
+ version: 3.22.4(@types/node@24.12.4)
pg:
specifier: 'catalog:'
version: 8.21.0
@@ -1597,7 +1603,7 @@ importers:
version: link:../validation
'@mantine/form':
specifier: 'catalog:'
- version: 9.2.1(react@19.2.6)
+ version: 9.2.2(react@19.2.6)
zod:
specifier: 'catalog:'
version: 4.4.3
@@ -1637,7 +1643,7 @@ importers:
version: link:../validation
'@mantine/core':
specifier: 'catalog:'
- version: 9.2.1(@mantine/hooks@9.2.1(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+ version: 9.2.2(@mantine/hooks@9.2.2(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
react:
specifier: 'catalog:'
version: 19.2.6
@@ -1703,7 +1709,7 @@ importers:
version: 8.0.0
'@ctrl/qbittorrent':
specifier: 'catalog:'
- version: 9.12.1
+ version: 9.12.2
'@ctrl/transmission':
specifier: 'catalog:'
version: 7.4.0
@@ -1797,10 +1803,10 @@ importers:
version: link:../ui
'@mantine/core':
specifier: 'catalog:'
- version: 9.2.1(@mantine/hooks@9.2.1(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+ version: 9.2.2(@mantine/hooks@9.2.2(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@mantine/hooks':
specifier: 'catalog:'
- version: 9.2.1(react@19.2.6)
+ version: 9.2.2(react@19.2.6)
react:
specifier: 'catalog:'
version: 19.2.6
@@ -1849,13 +1855,13 @@ importers:
version: link:../validation
'@mantine/core':
specifier: 'catalog:'
- version: 9.2.1(@mantine/hooks@9.2.1(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+ version: 9.2.2(@mantine/hooks@9.2.2(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@tabler/icons-react':
specifier: 'catalog:'
version: 3.44.0(react@19.2.6)
dayjs:
specifier: 'catalog:'
- version: 1.11.20
+ version: 1.11.21
next:
specifier: 'catalog:'
version: 16.2.6(@babel/core@7.29.0)(@playwright/test@1.60.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.100.0)
@@ -1883,7 +1889,7 @@ importers:
version: link:../ui
'@mantine/notifications':
specifier: 'catalog:'
- version: 9.2.1(@mantine/core@9.2.1(@mantine/hooks@9.2.1(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.2.1(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+ version: 9.2.2(@mantine/core@9.2.2(@mantine/hooks@9.2.2(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.2.2(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@tabler/icons-react':
specifier: 'catalog:'
version: 3.44.0(react@19.2.6)
@@ -1932,10 +1938,10 @@ importers:
version: link:../validation
'@mantine/core':
specifier: 'catalog:'
- version: 9.2.1(@mantine/hooks@9.2.1(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+ version: 9.2.2(@mantine/hooks@9.2.2(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@mantine/hooks':
specifier: 'catalog:'
- version: 9.2.1(react@19.2.6)
+ version: 9.2.2(react@19.2.6)
adm-zip:
specifier: 'catalog:'
version: 0.5.17
@@ -2016,7 +2022,7 @@ importers:
version: link:../definitions
ioredis:
specifier: 'catalog:'
- version: 5.10.1
+ version: 5.11.0
superjson:
specifier: 'catalog:'
version: 2.2.6
@@ -2056,7 +2062,7 @@ importers:
version: link:../redis
dayjs:
specifier: 'catalog:'
- version: 1.11.20
+ version: 1.11.21
octokit:
specifier: 'catalog:'
version: 5.0.5
@@ -2103,7 +2109,7 @@ importers:
version: link:../server-settings
'@mantine/dates':
specifier: 'catalog:'
- version: 9.2.1(@mantine/core@9.2.1(@mantine/hooks@9.2.1(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.2.1(react@19.2.6))(dayjs@1.11.20)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+ version: 9.2.2(@mantine/core@9.2.2(@mantine/hooks@9.2.2(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.2.2(react@19.2.6))(dayjs@1.11.21)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
next:
specifier: 'catalog:'
version: 16.2.6(@babel/core@7.29.0)(@playwright/test@1.60.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.100.0)
@@ -2144,6 +2150,9 @@ importers:
'@homarr/modals-collection':
specifier: workspace:^0.1.0
version: link:../modals-collection
+ '@homarr/notifications':
+ specifier: workspace:^0.1.0
+ version: link:../notifications
'@homarr/settings':
specifier: workspace:^0.1.0
version: link:../settings
@@ -2155,13 +2164,13 @@ importers:
version: link:../ui
'@mantine/core':
specifier: 'catalog:'
- version: 9.2.1(@mantine/hooks@9.2.1(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+ version: 9.2.2(@mantine/hooks@9.2.2(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@mantine/hooks':
specifier: 'catalog:'
- version: 9.2.1(react@19.2.6)
+ version: 9.2.2(react@19.2.6)
'@mantine/spotlight':
specifier: 'catalog:'
- version: 9.2.1(@mantine/core@9.2.1(@mantine/hooks@9.2.1(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.2.1(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+ version: 9.2.2(@mantine/core@9.2.2(@mantine/hooks@9.2.2(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.2.2(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@tabler/icons-react':
specifier: 'catalog:'
version: 3.44.0(react@19.2.6)
@@ -2198,19 +2207,19 @@ importers:
version: link:../definitions
dayjs:
specifier: 'catalog:'
- version: 1.11.20
+ version: 1.11.21
deepmerge:
specifier: 'catalog:'
version: 4.3.1
mantine-react-table:
specifier: 'catalog:'
- version: 2.0.0-beta.9(@mantine/core@9.2.1(@mantine/hooks@9.2.1(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/dates@9.2.1(@mantine/core@9.2.1(@mantine/hooks@9.2.1(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.2.1(react@19.2.6))(dayjs@1.11.20)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.2.1(react@19.2.6))(@tabler/icons-react@3.44.0(react@19.2.6))(clsx@2.1.1)(dayjs@1.11.20)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+ version: mantine-react-table-open@9.0.3(@mantine/core@9.2.2(@mantine/hooks@9.2.2(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/dates@9.2.2(@mantine/core@9.2.2(@mantine/hooks@9.2.2(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.2.2(react@19.2.6))(dayjs@1.11.21)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.2.2(react@19.2.6))(@tabler/icons-react@3.44.0(react@19.2.6))(clsx@2.1.1)(dayjs@1.11.21)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
next:
specifier: 'catalog:'
version: 16.2.6(@babel/core@7.29.0)(@playwright/test@1.60.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.100.0)
next-intl:
specifier: 'catalog:'
- version: 4.12.0(next@16.2.6(@babel/core@7.29.0)(@playwright/test@1.60.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.100.0))(react@19.2.6)(typescript@5.9.3)
+ version: 4.13.0(next@16.2.6(@babel/core@7.29.0)(@playwright/test@1.60.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.100.0))(react@19.2.6)(typescript@5.9.3)
react:
specifier: 'catalog:'
version: 19.2.6
@@ -2244,13 +2253,13 @@ importers:
version: link:../validation
'@mantine/core':
specifier: 'catalog:'
- version: 9.2.1(@mantine/hooks@9.2.1(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+ version: 9.2.2(@mantine/hooks@9.2.2(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@mantine/dates':
specifier: 'catalog:'
- version: 9.2.1(@mantine/core@9.2.1(@mantine/hooks@9.2.1(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.2.1(react@19.2.6))(dayjs@1.11.20)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+ version: 9.2.2(@mantine/core@9.2.2(@mantine/hooks@9.2.2(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.2.2(react@19.2.6))(dayjs@1.11.21)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@mantine/hooks':
specifier: 'catalog:'
- version: 9.2.1(react@19.2.6)
+ version: 9.2.2(react@19.2.6)
'@tabler/icons-react':
specifier: 'catalog:'
version: 3.44.0(react@19.2.6)
@@ -2259,7 +2268,7 @@ importers:
version: 4.2.0
mantine-react-table:
specifier: 'catalog:'
- version: 2.0.0-beta.9(@mantine/core@9.2.1(@mantine/hooks@9.2.1(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/dates@9.2.1(@mantine/core@9.2.1(@mantine/hooks@9.2.1(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.2.1(react@19.2.6))(dayjs@1.11.20)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.2.1(react@19.2.6))(@tabler/icons-react@3.44.0(react@19.2.6))(clsx@2.1.1)(dayjs@1.11.20)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+ version: mantine-react-table-open@9.0.3(@mantine/core@9.2.2(@mantine/hooks@9.2.2(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/dates@9.2.2(@mantine/core@9.2.2(@mantine/hooks@9.2.2(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.2.2(react@19.2.6))(dayjs@1.11.21)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.2.2(react@19.2.6))(@tabler/icons-react@3.44.0(react@19.2.6))(clsx@2.1.1)(dayjs@1.11.21)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
next:
specifier: 'catalog:'
version: 16.2.6(@babel/core@7.29.0)(@playwright/test@1.60.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.100.0)
@@ -2381,76 +2390,76 @@ importers:
version: link:../validation
'@mantine/charts':
specifier: 'catalog:'
- version: 9.2.1(@mantine/core@9.2.1(@mantine/hooks@9.2.1(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.2.1(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(recharts@3.8.1(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react-is@19.2.6)(react@19.2.6)(redux@5.0.1))
+ version: 9.2.2(@mantine/core@9.2.2(@mantine/hooks@9.2.2(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.2.2(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(recharts@3.8.1(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react-is@19.2.6)(react@19.2.6)(redux@5.0.1))
'@mantine/core':
specifier: 'catalog:'
- version: 9.2.1(@mantine/hooks@9.2.1(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+ version: 9.2.2(@mantine/hooks@9.2.2(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@mantine/form':
specifier: 'catalog:'
- version: 9.2.1(react@19.2.6)
+ version: 9.2.2(react@19.2.6)
'@mantine/hooks':
specifier: 'catalog:'
- version: 9.2.1(react@19.2.6)
+ version: 9.2.2(react@19.2.6)
'@tabler/icons-react':
specifier: 'catalog:'
version: 3.44.0(react@19.2.6)
'@tiptap/extension-color':
specifier: 'catalog:'
- version: 3.23.6(@tiptap/extension-text-style@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6)))
+ version: 3.24.0(@tiptap/extension-text-style@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0)))
'@tiptap/extension-highlight':
specifier: 'catalog:'
- version: 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))
+ version: 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))
'@tiptap/extension-image':
specifier: 'catalog:'
- version: 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))
+ version: 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))
'@tiptap/extension-link':
specifier: 'catalog:'
- version: 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)
+ version: 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)
'@tiptap/extension-placeholder':
specifier: 'catalog:'
- version: 3.23.6(@tiptap/extensions@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6))
+ version: 3.24.0(@tiptap/extensions@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0))
'@tiptap/extension-table':
specifier: 'catalog:'
- version: 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)
+ version: 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)
'@tiptap/extension-table-cell':
specifier: 'catalog:'
- version: 3.23.6(@tiptap/extension-table@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6))
+ version: 3.24.0(@tiptap/extension-table@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0))
'@tiptap/extension-table-header':
specifier: 'catalog:'
- version: 3.23.6(@tiptap/extension-table@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6))
+ version: 3.24.0(@tiptap/extension-table@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0))
'@tiptap/extension-table-row':
specifier: 'catalog:'
- version: 3.23.6(@tiptap/extension-table@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6))
+ version: 3.24.0(@tiptap/extension-table@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0))
'@tiptap/extension-task-item':
specifier: 'catalog:'
- version: 3.23.6(@tiptap/extension-list@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6))
+ version: 3.24.0(@tiptap/extension-list@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0))
'@tiptap/extension-task-list':
specifier: 'catalog:'
- version: 3.23.6(@tiptap/extension-list@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6))
+ version: 3.24.0(@tiptap/extension-list@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0))
'@tiptap/extension-text-align':
specifier: 'catalog:'
- version: 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))
+ version: 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))
'@tiptap/extension-text-style':
specifier: 'catalog:'
- version: 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))
+ version: 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))
'@tiptap/extension-underline':
specifier: 'catalog:'
- version: 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))
+ version: 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))
'@tiptap/react':
specifier: 'catalog:'
- version: 3.23.6(@floating-ui/dom@1.7.6)(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+ version: 3.24.0(@floating-ui/dom@1.7.6)(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@tiptap/starter-kit':
specifier: 'catalog:'
- version: 3.23.6
+ version: 3.24.0
clsx:
specifier: 'catalog:'
version: 2.1.1
dayjs:
specifier: 'catalog:'
- version: 1.11.20
+ version: 1.11.21
mantine-react-table:
specifier: 'catalog:'
- version: 2.0.0-beta.9(@mantine/core@9.2.1(@mantine/hooks@9.2.1(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/dates@9.2.1(@mantine/core@9.2.1(@mantine/hooks@9.2.1(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.2.1(react@19.2.6))(dayjs@1.11.20)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.2.1(react@19.2.6))(@tabler/icons-react@3.44.0(react@19.2.6))(clsx@2.1.1)(dayjs@1.11.20)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+ version: mantine-react-table-open@9.0.3(@mantine/core@9.2.2(@mantine/hooks@9.2.2(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/dates@9.2.2(@mantine/core@9.2.2(@mantine/hooks@9.2.2(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.2.2(react@19.2.6))(dayjs@1.11.21)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.2.2(react@19.2.6))(@tabler/icons-react@3.44.0(react@19.2.6))(clsx@2.1.1)(dayjs@1.11.21)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
next:
specifier: 'catalog:'
version: 16.2.6(@babel/core@7.29.0)(@playwright/test@1.60.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.100.0)
@@ -2621,6 +2630,10 @@ packages:
resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==}
engines: {node: '>=6.9.0'}
+ '@babel/runtime@7.29.7':
+ resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==}
+ engines: {node: '>=6.9.0'}
+
'@babel/template@7.28.6':
resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==}
engines: {node: '>=6.9.0'}
@@ -2696,8 +2709,8 @@ packages:
resolution: {integrity: sha512-fEfw3AeQ8XhVsNzogZrnyTNDD/Tpbfl3ybHsspBMvj0UKctabApv1JV3JJUQG/hoX6hiO+DtQS+acN7dMPX01g==}
engines: {node: '>20'}
- '@ctrl/qbittorrent@9.12.1':
- resolution: {integrity: sha512-6rSTWPV0UwehqLCyBLh9Avw5FPA1nh8SO+aTxrEXu6okZPjz+3tn7EGZdLhOfy+6poIc7miX/1g1VAOVo3JSTA==}
+ '@ctrl/qbittorrent@9.12.2':
+ resolution: {integrity: sha512-eU/SLUYmD4vixvwdLe/u+Ks2ucd8n9ZouKLFyVX9VOMz4Q6syvccwoS2/WobDyTCWLuhEBXIqF2N4NOUUi0LBA==}
engines: {node: '>=18'}
'@ctrl/shared-torrent@6.5.1':
@@ -3264,15 +3277,9 @@ packages:
'@fastify/proxy-addr@5.1.0':
resolution: {integrity: sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==}
- '@floating-ui/core@1.7.3':
- resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==}
-
'@floating-ui/core@1.7.5':
resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==}
- '@floating-ui/dom@1.7.4':
- resolution: {integrity: sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==}
-
'@floating-ui/dom@1.7.6':
resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==}
@@ -3309,6 +3316,15 @@ packages:
'@formatjs/intl-localematcher@0.8.1':
resolution: {integrity: sha512-xwEuwQFdtSq1UKtQnyTZWC+eHdv7Uygoa+H2k/9uzBVQjDyp9r20LNDNKedWXll7FssT3GRHvqsdJGYSUWqYFA==}
+ '@gfazioli/mantine-onboarding-tour@4.0.2':
+ resolution: {integrity: sha512-x6mcsI6qyJEXeQfmeM9yMIyWoKpLzfrhYWU04LuShNPjvrF3NprTYzmxpUsRGpdSUNVnVXwEku4ZD5u7j0tx8Q==}
+ peerDependencies:
+ '@mantine/core': '>=7.0.0'
+ '@mantine/hooks': '>=7.0.0'
+ '@tabler/icons-react': ^3.30.0
+ react: ^18.x || ^19.x
+ react-dom: ^18.x || ^19.x
+
'@gitbeaker/core@43.8.0':
resolution: {integrity: sha512-H+LfKuf4dExBinb79c+CXViRBvTVQNf5BYLNSizm2SiqdED5JruhKX88payefleY0szp7G/mySlFSXPyGRH1dQ==}
engines: {node: '>=18.20.0'}
@@ -3665,8 +3681,8 @@ packages:
'@types/node':
optional: true
- '@ioredis/commands@1.5.1':
- resolution: {integrity: sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw==}
+ '@ioredis/commands@1.10.0':
+ resolution: {integrity: sha512-UmeW7z4LfctwoQ5wkhVzgq8tXkreED2xZGpX+Bg+zA+WJFZCT6c062AfCK/Dfk81xZnnwdhJCUMkitihRaoC2Q==}
'@isaacs/cliui@8.0.2':
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
@@ -3729,88 +3745,88 @@ packages:
'@libsql/core@0.14.0':
resolution: {integrity: sha512-nhbuXf7GP3PSZgdCY2Ecj8vz187ptHlZQ0VRc751oB2C1W8jQUXKKklvt7t1LJiUTQBVJuadF628eUk+3cRi4Q==}
- '@mantine/charts@9.2.1':
- resolution: {integrity: sha512-AVP0VEfcsbWwLBeU8rbEsN2gz3GebQECTreRq5AJ0Z5V1eWmTO7XFkRP9C5C6oHnkY4Vppa1x8cRYgyTsc/YbQ==}
+ '@mantine/charts@9.2.2':
+ resolution: {integrity: sha512-F152Lf3f6H62mwzihBMobjizj0ShdL6VasqI0DjPaCmw39NmI1xHdaA2ydp8YI6TwZFCWVQHR5psPpaMdTHBCA==}
peerDependencies:
- '@mantine/core': 9.2.1
- '@mantine/hooks': 9.2.1
+ '@mantine/core': 9.2.2
+ '@mantine/hooks': 9.2.2
react: ^19.2.0
react-dom: ^19.2.0
recharts: '>=3.2.1'
- '@mantine/colors-generator@9.2.1':
- resolution: {integrity: sha512-kfO8uxxBbHGJdrZtcpfsJ/K6+9xeNMsGa01o2IgOGOU2fOIn8wCkbGL9sdcfkurwVhipmf8LNQJiPHHfmbmBpQ==}
+ '@mantine/colors-generator@9.2.2':
+ resolution: {integrity: sha512-13Tv1TTDJ1IaneYY2S5uqZ4ICq75k8D/lTyVEFQSB5yDU7npBIetrVG2mT8wJjpYJz4CHZmINxxv0abP5N3p1w==}
peerDependencies:
chroma-js: '>=2.4.2'
- '@mantine/core@9.2.1':
- resolution: {integrity: sha512-CicPg9i2dM2pGp1jj+dMiR/63OFDsPjgJke4v5+0nbfJ+C7gn4C+7ltrp4RIETDMZHcj0fFuDRG0qtbiyBxvWA==}
+ '@mantine/core@9.2.2':
+ resolution: {integrity: sha512-j4d7dwKkrYPP9Lb7ut1u0OodwmosIaKK4azK4GMoTKq1+7TKRrJPxB1vkDjkbOcd+jbUt0qn6EkJdsSMKVcIIw==}
peerDependencies:
- '@mantine/hooks': 9.2.1
+ '@mantine/hooks': 9.2.2
react: ^19.2.0
react-dom: ^19.2.0
- '@mantine/dates@9.2.1':
- resolution: {integrity: sha512-cyRC8bnZ6W+SzQf/RM+/eDeWhZTZF148z+OcgqiERdkAujh0q88Ddybv/Hshv1EOf0rCe6oiHSZWxIxmUf7KAg==}
+ '@mantine/dates@9.2.2':
+ resolution: {integrity: sha512-6/O5f/u2NjJLKoLvZmSJCo6qQqPg627XjfpeUYqOx/h9I2W36eVaKghM4YObPpfkPZMIojaa3iz82Nw3lmZO3w==}
peerDependencies:
- '@mantine/core': 9.2.1
- '@mantine/hooks': 9.2.1
+ '@mantine/core': 9.2.2
+ '@mantine/hooks': 9.2.2
dayjs: '>=1.0.0'
react: ^19.2.0
react-dom: ^19.2.0
- '@mantine/dropzone@9.2.1':
- resolution: {integrity: sha512-rgebEz2bUubqA5jQpsh0SZ9/4DJFnUSF+a4p8uzAVlgNfo6aU/r+I4lEsD8gzLKuxrfN0TgkwR1qmAjLWIy0yQ==}
+ '@mantine/dropzone@9.2.2':
+ resolution: {integrity: sha512-mYpaOfCrrOO1CytVbBaAxVRntHmB+YhjFlu1SGIIu9eoPr1pKOopvhVg58iiDG+xPIAzAH6zySEGRsTvPN61bw==}
peerDependencies:
- '@mantine/core': 9.2.1
- '@mantine/hooks': 9.2.1
+ '@mantine/core': 9.2.2
+ '@mantine/hooks': 9.2.2
react: ^19.2.0
react-dom: ^19.2.0
- '@mantine/form@9.2.1':
- resolution: {integrity: sha512-PV0dcbmsKhZkn3Ryztqp8Kb1N16v2nWXO71p4utTmN7E/lHWHO1ccj7Y72sIteWJo1YeNHzzkssaYRSnxgOvYA==}
+ '@mantine/form@9.2.2':
+ resolution: {integrity: sha512-Mfw4XgNZdd67AwLbRU540RiXybZ2vj1UDFh+ky2ebZwieTwP+kd2ePSQQjEp7hUnvby06GXPByVrg8eNGSq67w==}
peerDependencies:
react: ^19.2.0
- '@mantine/hooks@9.2.1':
- resolution: {integrity: sha512-IX/ztVG9eWmQTRsN7G8odyW4JckNvN8qv5A2ULzXyazjtAKLuaUpuMz0c6XhRp10J0g4bVfV3rhrTgWeImqxqg==}
+ '@mantine/hooks@9.2.2':
+ resolution: {integrity: sha512-E0eKEMv47Xo+QVKYj2buzLJURcO/Qfb12EvM4mjKlXPuTqYhl+Fvg7Sg+xHmta8iTtBvo/5G+iNeqc4pfs8eaA==}
peerDependencies:
react: ^19.2.0
- '@mantine/modals@9.2.1':
- resolution: {integrity: sha512-YvZ85ZtMg6arFserkmmP18gJRD9ztLLT3vz8UrkwCdVwTGGr14X93hhS0UtR5X96ANXUzC8fU/S2ncQmNqw+NQ==}
+ '@mantine/modals@9.2.2':
+ resolution: {integrity: sha512-OiLHaDrHw7ArCBXzxap+lsMdGwLiyOWD+HQ9pqwwyczmvwcgVgqcFi4gq4eOs8GGZc/e6WaQ0uKbYvlVBgK5wA==}
peerDependencies:
- '@mantine/core': 9.2.1
- '@mantine/hooks': 9.2.1
+ '@mantine/core': 9.2.2
+ '@mantine/hooks': 9.2.2
react: ^19.2.0
react-dom: ^19.2.0
- '@mantine/notifications@9.2.1':
- resolution: {integrity: sha512-H6lSsKUPdWPYcXFeOcqqcegUl72Iua9yD369s/gchfDvI+PrL4IM5UuWNHTeABJ1eb9FbTOw8B5RDSMZjTZNSA==}
+ '@mantine/notifications@9.2.2':
+ resolution: {integrity: sha512-7pNVA1wy+QJmzQiXXP65o9pzg/NLpLNU9wE0nLU2chjpvuqvfu1KP1lOB3097rebLxYmmrE25Dn4hxfSrHwCmw==}
peerDependencies:
- '@mantine/core': 9.2.1
- '@mantine/hooks': 9.2.1
+ '@mantine/core': 9.2.2
+ '@mantine/hooks': 9.2.2
react: ^19.2.0
react-dom: ^19.2.0
- '@mantine/spotlight@9.2.1':
- resolution: {integrity: sha512-D6/BJ8/ftUBwiSFGTtYUM/LqlBk40XZqZFlmEFRM8chDPzEId1O+5FU7mj8KapPR9zRgoAcdT4LMI6JaLp0c0A==}
+ '@mantine/spotlight@9.2.2':
+ resolution: {integrity: sha512-kgXAu5ODsduuieraRWUtfjGLnxDssvk5WhrWUxlVcDTaBjP+PdeXvkSCzBR73mShEvRidBxKml7Ru9Sx7OBK9g==}
peerDependencies:
- '@mantine/core': 9.2.1
- '@mantine/hooks': 9.2.1
+ '@mantine/core': 9.2.2
+ '@mantine/hooks': 9.2.2
react: ^19.2.0
react-dom: ^19.2.0
- '@mantine/store@9.2.1':
- resolution: {integrity: sha512-sBTHt9ilfSZAeXQlqFkm8nRm22RunhevxuOUtdSwS9HhuMuS8T27dRRgbdKH2oEFUbaccdQSy5bHbmGbEgVO8w==}
+ '@mantine/store@9.2.2':
+ resolution: {integrity: sha512-ZzS1yERapPURUG1vs7G+lD+oc8AZKissP1eUzefpyOAgEoEJXjOckc5/voiik3dcc/WP4AE9GQsT3Z4cS27E/A==}
peerDependencies:
react: ^19.2.0
- '@mantine/tiptap@9.2.1':
- resolution: {integrity: sha512-UzztgwqIknJheF39cqG2AgPI1qEvXIa7jNFuij8iB6GLd0UMmCjKnW42Px0WAaHuNfMjweml9IyTLBTSvIK0uA==}
+ '@mantine/tiptap@9.2.2':
+ resolution: {integrity: sha512-+Ki/+TVSATME+3V4zpAEToHvEcJilE2RNZwo183wYNaHTsdn6WgGOHsUrdPV0LSV8AajZwBBIE5mJaStVVZXvg==}
peerDependencies:
- '@mantine/core': 9.2.1
- '@mantine/hooks': 9.2.1
+ '@mantine/core': 9.2.2
+ '@mantine/hooks': 9.2.2
'@tiptap/extension-link': '>=3.3.0'
'@tiptap/react': '>=3.3.0'
react: ^19.2.0
@@ -4004,246 +4020,246 @@ packages:
'@open-draft/until@2.1.0':
resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==}
- '@oxfmt/binding-android-arm-eabi@0.51.0':
- resolution: {integrity: sha512-Ni0sCqg5CIHaLIYFGj+ncbcumylvNC6FE4rfD0KfdmnWHbPJ+zev0qZCXKxy2hFVa0fYRK0yPzf5nzPbkZou7g==}
+ '@oxfmt/binding-android-arm-eabi@0.52.0':
+ resolution: {integrity: sha512-17EMSJnQ9g+upVHrAUYDMfH5lvRKQ9Nvg8WtEoH72oDr1VpWz+7/o3tD97U1EToen2YAQ/68JmtDYkQUi20dfQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm]
os: [android]
- '@oxfmt/binding-android-arm64@0.51.0':
- resolution: {integrity: sha512-eu5lAZjuo0KAkp+M24EhDqfOwA8owQ8d7wyBlOUUGRbDLHpU3IRlDHp8Dif+YqGlxs6jra7yS6WQu/NkPhAxeg==}
+ '@oxfmt/binding-android-arm64@0.52.0':
+ resolution: {integrity: sha512-A2G1IdwGEW2lLJkIxcvuirRH1CzSl/e0NX11zTlW1gvxJThfwbI/BEoaKrTNpm7M2FchvIf6guvIQU7d5iz+OQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [android]
- '@oxfmt/binding-darwin-arm64@0.51.0':
- resolution: {integrity: sha512-6LsUNIdURhhcIfIn8+xsOb61mSTa9msAHTeSGx9Jf4rsP/gN8PGCF+SKWPAQZbND2w/WBkqQ6303jqEEIXzMdQ==}
+ '@oxfmt/binding-darwin-arm64@0.52.0':
+ resolution: {integrity: sha512-f9+bLvOYxy7NttCLFTvQ7afmqDOWY4wIP9xdvfj5trQ1qj6f2UFAGwZESlfsMjvJNTyRpXfIlOanCI9FOvoeQA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [darwin]
- '@oxfmt/binding-darwin-x64@0.51.0':
- resolution: {integrity: sha512-9aUMGmVxdHjYMsEAW1tNRoieTJXlVNDFkRvIR1J7LttJXWjVYCu2ekclLij2KJtxBxSQOYSHd12ME/adVGVbZg==}
+ '@oxfmt/binding-darwin-x64@0.52.0':
+ resolution: {integrity: sha512-YSTB9sJ5nnQd/Q0ddHkgof0ZCHPAnWZT1IW2SJ8omz7CP7KluJhO1fNHrpqdxCtpztJwSs4hY1uAee35wKxxaw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [darwin]
- '@oxfmt/binding-freebsd-x64@0.51.0':
- resolution: {integrity: sha512-mkY1nhZTqYb+NHaAWxOCKISN6FwdrwMNsu17vTUA3wzUV2VJ+Paq15ZokRcsMU/2PUdHO73prxyeJpjXQ3MPpQ==}
+ '@oxfmt/binding-freebsd-x64@0.52.0':
+ resolution: {integrity: sha512-NIrRNTTPCs4UbmVs0bxLSCDlLCtIRMJIXklNKaXa5Oj2/K1UIMBvgE8+uPVo01Io3N9HF0+GAX+aAHjUgZS7vA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [freebsd]
- '@oxfmt/binding-linux-arm-gnueabihf@0.51.0':
- resolution: {integrity: sha512-wtFwNwE4+YCNuPaWoGDZeGsKvD6D1YSUNBJNn/rJBh7CrDBThFE+TBI5kY7vRW9rIOQRsbW2IpyyL3Du4Zqwiw==}
+ '@oxfmt/binding-linux-arm-gnueabihf@0.52.0':
+ resolution: {integrity: sha512-JXUCde8mn3GpgQouz2PXUokgy/uT1QrRJBL2s983VWcSQp62wTFYiNXgTKdeo1Jgbr0IgUnKKvzIk/YBlj/nVQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm]
os: [linux]
- '@oxfmt/binding-linux-arm-musleabihf@0.51.0':
- resolution: {integrity: sha512-rnOaNx86G7iRKM6lsCIQMux0SMGNC/TEbFR+r7lpruJ12bnrIWgxd5w1PLqOvgR9r8ZJbpK/zfRKctJnh8/Jfg==}
+ '@oxfmt/binding-linux-arm-musleabihf@0.52.0':
+ resolution: {integrity: sha512-psbUXaRZ+V8DaXz10Qf7LSHtdtdKAmC8fxXgeU608jjzrmWK4quamZMOpl6sf+dikoFHA85uE93Q0BqxrCdQrQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm]
os: [linux]
- '@oxfmt/binding-linux-arm64-gnu@0.51.0':
- resolution: {integrity: sha512-jOgDzSqWcICGRjsp4mc08FxKMN8vzP2Kgs4E0d2HUP99F+nJDQKklRV4Zuj+0gcBgjrzx2CbpqaIdUVPepCojA==}
+ '@oxfmt/binding-linux-arm64-gnu@0.52.0':
+ resolution: {integrity: sha512-Jw7MgWUU9lcLCcy82updISP3EthTlfvAwR6gWNxPzqly7+fLvOi2gHQE9xXQjpqaVLm/8P+gOzlv9ODuoVlaaw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
- '@oxfmt/binding-linux-arm64-musl@0.51.0':
- resolution: {integrity: sha512-KBUCdrH5bwVrAvI9gU/1S55oH6fzXjr++J/oVocdu7bYTks1l7DNNT+rLd/1TDdAEjObGwmfWamn7LC1m8A0DQ==}
+ '@oxfmt/binding-linux-arm64-musl@0.52.0':
+ resolution: {integrity: sha512-wZg6bLjDvh2KibyI3QFUYo8GTXneIFsd0JvehtvJiUmQ8WRPERgxd/VM4ctWb86U5FT1FkqgS8/wZKVB+AZScg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [musl]
- '@oxfmt/binding-linux-ppc64-gnu@0.51.0':
- resolution: {integrity: sha512-NapfjYsABFqTJ1Dn9Efq6sN5esaHconVKwVLbDGNQLrwpOx/g17mkwErHzU72PutL67nf3wNAkbq122H+zLxag==}
+ '@oxfmt/binding-linux-ppc64-gnu@0.52.0':
+ resolution: {integrity: sha512-IngE8uxhNvxcMrLjZNDo9xNLY7rEK33AKnaMd2B46he1e/mz2CfcW6If/U1wUjdRZddm1QzQaciqZkuMkdh1FA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [ppc64]
os: [linux]
libc: [glibc]
- '@oxfmt/binding-linux-riscv64-gnu@0.51.0':
- resolution: {integrity: sha512-5dlDt1dUZCVi6elIhiK1PWg9wpTzTcIuj0IZnSurvIoMrhOWqqTcc1dSTxcSkNaBZhfsNqRZdINI1zAgbKkJNQ==}
+ '@oxfmt/binding-linux-riscv64-gnu@0.52.0':
+ resolution: {integrity: sha512-H3+DdFMv/efN3Efmhsv18jDrpiWWqKG7wsfAlQBqAt6z/E2Bx+TwEj2Nowe51CPOWB8/mFBC2dAMSgVFLvvowA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [riscv64]
os: [linux]
libc: [glibc]
- '@oxfmt/binding-linux-riscv64-musl@0.51.0':
- resolution: {integrity: sha512-pgdWUJn0S5nulyiVdlFV8DzCUnGXkU99W5PSkkmbaZW+LrZBPxpezun4G0DDHbQaVYuJeCuKsXsGKGo77CkUTQ==}
+ '@oxfmt/binding-linux-riscv64-musl@0.52.0':
+ resolution: {integrity: sha512-zji+1kb7lJKohSDjzC1IsS+K/cKRs1hdVf0ZH0VbdbiakmtLvN9twBoXo/k8VdjFax7kfo+DyPxS7vv52br1aw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [riscv64]
os: [linux]
libc: [musl]
- '@oxfmt/binding-linux-s390x-gnu@0.51.0':
- resolution: {integrity: sha512-2XTFUe97CbDGAI8vjwDfZ1HdakO0XIADyJ24idEg64SC4/K4in/OisXVnrW4NMK7I6TgC7EqRhC0Ln/nKhAemA==}
+ '@oxfmt/binding-linux-s390x-gnu@0.52.0':
+ resolution: {integrity: sha512-hcLBYedpCy7ToUvvBidWk7+11Yhg1oAZ4+6hKPic/mQI6NaqXJSXMps5nFlwUuX2ewhtLZZDPg63TI042qGKBg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [s390x]
os: [linux]
libc: [glibc]
- '@oxfmt/binding-linux-x64-gnu@0.51.0':
- resolution: {integrity: sha512-kQ1OuCqqt/yyf0ZN9VFxW1/JnlgJgii3Dr7pWf9vNBvrX1hv6g39/+mc5oGRHRGJFZtl3zsGDWR9c5N2B/gwBw==}
+ '@oxfmt/binding-linux-x64-gnu@0.52.0':
+ resolution: {integrity: sha512-IDO2loXK2OtTOhSPchU9MW25mWL2QCDGdJbjN8MXKZVS80qXe5gMTwQWu/gMJ3juoBHbkuUZNB2N1LHzNT7DoA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [glibc]
- '@oxfmt/binding-linux-x64-musl@0.51.0':
- resolution: {integrity: sha512-ARTYqxHF475o96Gbn41hvSWSSRygPlRDXZZgZ9I2scU1y0qiWpCQyZCoefaQa0mwv+wwtZ+luS4YOzsRzM/izg==}
+ '@oxfmt/binding-linux-x64-musl@0.52.0':
+ resolution: {integrity: sha512-mAV2Hjn0SatJ+KoAzKUC3eJhdJ8wv+3m1KyuS0dTsbF0c5weq+QrCt/DRZZM+uj/XiKzCDEUKYsBF30e2qkcyw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [musl]
- '@oxfmt/binding-openharmony-arm64@0.51.0':
- resolution: {integrity: sha512-QiC1XrCl6a6BmqMzduO8hdIRMf1m44hCkt2Q68KWkTvUB/E7fd2iomyNh6KnnRca5w6eBrRAAtLFqTh+xjsjJA==}
+ '@oxfmt/binding-openharmony-arm64@0.52.0':
+ resolution: {integrity: sha512-vd4npaUIwChxp7XzkqmepBWTT9YMcSe/NBApVGPC30/lLyOVaV3dvma1SKo03t8O73BPRAG7EyJzGlN5cJM5hQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [openharmony]
- '@oxfmt/binding-win32-arm64-msvc@0.51.0':
- resolution: {integrity: sha512-NC/hJb9dtU23Zf8L7IVK95xnFjiQ7AfcLO2l5pb69TDEr958qxrtnB2CveeeNSCBFNIkgaTCfd/vHNSoG78l9g==}
+ '@oxfmt/binding-win32-arm64-msvc@0.52.0':
+ resolution: {integrity: sha512-k2sz6gWQdMfh5HPpIS+Bw/0UEV/kaK2xuqJRrWL233sEHx9WLlsmvlPFM4HUNThkYbSN0U0vPW7LVKZWDS8hPQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [win32]
- '@oxfmt/binding-win32-ia32-msvc@0.51.0':
- resolution: {integrity: sha512-2C45za4Rj36n8YIbhRL1PQbxmXJYf81WEcAgvj5I4ptRROG+A+81hREEN5bmCHADE1UfYaN312U6tkILoZZy6w==}
+ '@oxfmt/binding-win32-ia32-msvc@0.52.0':
+ resolution: {integrity: sha512-rhke69GTcArodLHpjMTfNnvjTEBryDeZcUCKK/VjXDMtfTULl6QRh0ymX5/hbCUv2WjYm9h/QbW++q2vE15gWQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [ia32]
os: [win32]
- '@oxfmt/binding-win32-x64-msvc@0.51.0':
- resolution: {integrity: sha512-73RqdAuVKQTkjZIDw08JaDHUM4lav5Qu+CaPwg4QbbA7k8o7LEW0p3UsfZ/F8dsO/pwVYh3RzFcanwLRTTahbQ==}
+ '@oxfmt/binding-win32-x64-msvc@0.52.0':
+ resolution: {integrity: sha512-q5xL7oeXkZdEtNZWBdvehJcmt+GRu9l2bK40yJs1jJXlqq+r0Hygb1rTjq+FM2o/2xyt4cufH6KRplHp3Jjsvw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [win32]
- '@oxlint/binding-android-arm-eabi@1.66.0':
- resolution: {integrity: sha512-f7kq8N51T4phpzqfBpA2qaVTI/KrkCmNwaj3t/97I/WLTDI+UhlP5GL9eER+zVxBhtlx5rKXWByJU1/zDAvyaw==}
+ '@oxlint/binding-android-arm-eabi@1.67.0':
+ resolution: {integrity: sha512-VrSi571rDv1N8HaEDM+DEX8nmT0y9jJo8tzzW13vsOWTx59xQczCIJx68n2zWOXRT5YKZsOZXp4qkHN/10x4mw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm]
os: [android]
- '@oxlint/binding-android-arm64@1.66.0':
- resolution: {integrity: sha512-xu6QO71tdDS9mjmLZ3AqhtaVHBvdmsOKkYnReNNDgh+XiwnsipeQOIxbiYOOO0iAXycJ+GK0wdMSZP/2j/AmSg==}
+ '@oxlint/binding-android-arm64@1.67.0':
+ resolution: {integrity: sha512-l6+NdYxMoRohix5r5bbigW16LPicceCwGcQ6LKKuE1kUdjgFfQolJjrJsQYPFetIs78Gxj/G/f5TEGoTCwj9nQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [android]
- '@oxlint/binding-darwin-arm64@1.66.0':
- resolution: {integrity: sha512-HZ24VimSOC7mxuEA99e0H2FS0C1yO3+iW13jPRAk+e2njsUs3QeAXsafCDyaIrV/MirdOVez+etQNQsJE43zNQ==}
+ '@oxlint/binding-darwin-arm64@1.67.0':
+ resolution: {integrity: sha512-jOzXxS1AxFxhImLIRbtGIMrEwaXcgMw3gR57WB1cRk8ai+vpr6726kxXqVvlNsrXtJ/FrmOm8RxlC0m8SW24Qg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [darwin]
- '@oxlint/binding-darwin-x64@1.66.0':
- resolution: {integrity: sha512-awhj8ZvJrrRSnXj7V++rpZvTmnl99L6mi0B7gg7Cp7BN6cKpzuI481bHNLvXGA9GB1/oEgA3ponuyoAc6Md12A==}
+ '@oxlint/binding-darwin-x64@1.67.0':
+ resolution: {integrity: sha512-3DFAVY94OqjIZHXIPz37yGRSWwOFTAqChQ64/M69GYLawzP0KiwdhDNfqdKKYT0bTR/DNxmMnQsj3ns+8+X/Lg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [darwin]
- '@oxlint/binding-freebsd-x64@1.66.0':
- resolution: {integrity: sha512-KQF0oVV21/FjIqkRuL8Q1vh8ECsE5+ocdH5tcqTQ4ZnYuDVoYibQUNfqBjQaUsP6UIIda5Y75Wpm5p4RgQWiWw==}
+ '@oxlint/binding-freebsd-x64@1.67.0':
+ resolution: {integrity: sha512-e4dDKZuLu8TR9DEBssWSDahlPgZBwojTTHZUvnjBRJfJJbpxYCjfjKfi0Z1+CSLMiJBwI2yCDtRM1XJQaARjmg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [freebsd]
- '@oxlint/binding-linux-arm-gnueabihf@1.66.0':
- resolution: {integrity: sha512-9u1rgwZSEXWb30vbFZzQ78HVXBo0WCKNwJ3a2InRUTNMRng+PUDIoSFmA+m4HdUfBaIqftShq8J8qHc+eE/Vig==}
+ '@oxlint/binding-linux-arm-gnueabihf@1.67.0':
+ resolution: {integrity: sha512-BKytFdcQzbITV3xlnzDUDTEDtbUMCCiC4EaNTDZ4FyT8gdNvBC4gfiLucXp/sQl0XU3p7syTlorUWVVVBZab2g==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm]
os: [linux]
- '@oxlint/binding-linux-arm-musleabihf@1.66.0':
- resolution: {integrity: sha512-Ynot2HR1bHxUaNWoC280MVTDfZuaWuP3XfSMRDhyuZrVjhzoaBCVFlw8h8qeZjWKVUBhPWFIxB7AQTlK8Z2WWg==}
+ '@oxlint/binding-linux-arm-musleabihf@1.67.0':
+ resolution: {integrity: sha512-XYAv0esBDX7BpTzRDjVX2Vdj+zndd8ll2dFQiaeQ6zTZr7A8GRDTN7fH3FP3jU+O0vCDx85oH/EtG7BzPgAXuw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm]
os: [linux]
- '@oxlint/binding-linux-arm64-gnu@1.66.0':
- resolution: {integrity: sha512-xCbgzciGgo+A4aQZEknsNrNiIwY7sU5SfRuMmRjPIvZAgdF34cIHiKvwOsS5XRLjlTVSFwitmq6YclTtHTfU+g==}
+ '@oxlint/binding-linux-arm64-gnu@1.67.0':
+ resolution: {integrity: sha512-zizRMjA0i6u/2B0evgda04iycu+MoNuf1pBy6Eh+1CjC5wMEG7qN5zdDKTCvFc0KSYSDM9QTG3gjZHirgtQuKg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
- '@oxlint/binding-linux-arm64-musl@1.66.0':
- resolution: {integrity: sha512-hmo+ZB/lHkR1HdDmnziNpzSLmulnUSu10VEqX2Yex7OwvoBAbjJQLvy4gIBRV3AAwWnCvAxKp5Nv1GE6LU1QMg==}
+ '@oxlint/binding-linux-arm64-musl@1.67.0':
+ resolution: {integrity: sha512-zB/Tf6sUjmmvvbva9Gj3JTJ8rJ9t4I8/U0o6vSRtd0DRIsIuyegBwJAzhSUFQHdMijIRJkW0exs/yBhpw2S20w==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [musl]
- '@oxlint/binding-linux-ppc64-gnu@1.66.0':
- resolution: {integrity: sha512-2Invd4Uyy81mVooQC5FBtfxSNrvcX1OxbMlVQ6M2erRrNI2awFYF26YNW2yFxdVFZ4ffNOWKghtMjhnUPsXsVA==}
+ '@oxlint/binding-linux-ppc64-gnu@1.67.0':
+ resolution: {integrity: sha512-kgU40Gt74CK0TCsF51KZymkIwN9U0BajKsMijB52zPqOeZU9NAHkA/NSQkZDHEaCakx42DxhXkODiAqf2b4Gug==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [ppc64]
os: [linux]
libc: [glibc]
- '@oxlint/binding-linux-riscv64-gnu@1.66.0':
- resolution: {integrity: sha512-s0iXPDQVdgayE3RGa/N2DZF7tjgg0TwEtD1sGoDxqPDGrIXgo45H0yHknT0f9A0yteASsweYZtDyTuVlM4aSag==}
+ '@oxlint/binding-linux-riscv64-gnu@1.67.0':
+ resolution: {integrity: sha512-tOYhkk/iaG9aD3FvGpBFd1Lrw0x0RaVoJBxjUkfNzS50rC5NS5BteNCwgr8A2zCdADrIIoze6D7u6U5Ic++/iQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [riscv64]
os: [linux]
libc: [glibc]
- '@oxlint/binding-linux-riscv64-musl@1.66.0':
- resolution: {integrity: sha512-OekL4XFiu7RPK0JIZi8VeHgtIXPREf42t8Cy/rKEsC+P3gcqDgNAAGiyuUOpdbG4wwbfue1q4CHcCO7spSve6w==}
+ '@oxlint/binding-linux-riscv64-musl@1.67.0':
+ resolution: {integrity: sha512-sEtywrPb+0b+tHYl1SDCrw903fiC4eyKoNqzP3v+f2JT3Xcv4NEYG+P8rj+eEnX7IWhqV/xj8/JmcmVj21CXaA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [riscv64]
os: [linux]
libc: [musl]
- '@oxlint/binding-linux-s390x-gnu@1.66.0':
- resolution: {integrity: sha512-Ga1D0kj1SFslm34ThA/BdkUlyAYEnTsXyRC4pF0C5agZSwtGdHYWMTQWemUfBGp4RCG4QWXgdO+HmmmKqOtlBg==}
+ '@oxlint/binding-linux-s390x-gnu@1.67.0':
+ resolution: {integrity: sha512-BvR8Moa0zCLxroOx4vZaZN9nUfwAUpSTwjZdxZyKy4bv3PrzrXrxKR/ZQ0L9wNSvlPhnMJeZfa3q5w6ZCTuN6Q==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [s390x]
os: [linux]
libc: [glibc]
- '@oxlint/binding-linux-x64-gnu@1.66.0':
- resolution: {integrity: sha512-p5jfP1wUZe/IC3qpQO84n9DRnf9g3lKRtLBlQq23ykyrDglHcVx7sWmVTlPuU6SBw8mNnPzyOn022G3XZHnlww==}
+ '@oxlint/binding-linux-x64-gnu@1.67.0':
+ resolution: {integrity: sha512-mm2cxM6fksOpq6l0uFws8BUGKAR4dNa/cZCn37Npq7PFbhD5HDJqWfnoIvTaeRKMy5XdS2tO0MA0qbHDrnXAAA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [glibc]
- '@oxlint/binding-linux-x64-musl@1.66.0':
- resolution: {integrity: sha512-vUB/sYlYZorDL1ZD+o9mRv7zbsykrrFRtmgS6R8musZqLtrPRQn1gc1eGpuX+sfdccz42STl/AqldY6XRb2upQ==}
+ '@oxlint/binding-linux-x64-musl@1.67.0':
+ resolution: {integrity: sha512-WmbMuLapKyDlobMkXAaAL0Y+Uczh4LETfIfQsUpbId4Ip8Ai82/jqeYTOoUCkuuhBFapgqP253+d83tLKOksJg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [musl]
- '@oxlint/binding-openharmony-arm64@1.66.0':
- resolution: {integrity: sha512-yde+6p/F59xRkGR9H1HfngWRif1QRJjynZK349l+UI0H6w9hL3G8/AVaTHFyTtLVQ56qtNbX2/5Dc77n1ovnOg==}
+ '@oxlint/binding-openharmony-arm64@1.67.0':
+ resolution: {integrity: sha512-9g/PqxYJelzzTAOR5Y+RiRqdeydhEuXv2KxNeFcAKQ7UsvnWSY1OP4MsuPMbTO2Pf70tz7mFhl1j13H3fyh+8g==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [openharmony]
- '@oxlint/binding-win32-arm64-msvc@1.66.0':
- resolution: {integrity: sha512-O9GLucgoTdmOrbBX+EjzNe7o/Ze5TFOvXcib6bzUOtBOmj6cV+zw18NgB+cGKAkDw1Pdqs8vGkfHbbsLuDtXWg==}
+ '@oxlint/binding-win32-arm64-msvc@1.67.0':
+ resolution: {integrity: sha512-2VhwE6Gatb0vJGnN0TBuQMbKCOiZlSQ/zJvVWYLK4a9d4iDiJOen/yVQkGpmsJ90MuH66fzi0kEKI0jRQMDxGA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [win32]
- '@oxlint/binding-win32-ia32-msvc@1.66.0':
- resolution: {integrity: sha512-m3Pjwc2MfTcom4E4gOv7DyuGyt7OfGNCbmqDHd+N7EzXmP+ppHuudm2NjcA3AjV5TSeGxaguVF4SbTKHe1USYA==}
+ '@oxlint/binding-win32-ia32-msvc@1.67.0':
+ resolution: {integrity: sha512-EQ3VExXfeM1InbE5+JjufhZZTWy+kHUwgt3yZR7gQ47Je/mE0WspQPan0OJznh493L5anM210YNJtH1PXjTSFg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [ia32]
os: [win32]
- '@oxlint/binding-win32-x64-msvc@1.66.0':
- resolution: {integrity: sha512-/DbBvw8UFBhja6PqudUjV4UtfsJr0Oa7jUjWVKB0g86lj/VwnPrkngn0sFql3c9RDA0O16dh7ozsXb6GjNAzBQ==}
+ '@oxlint/binding-win32-x64-msvc@1.67.0':
+ resolution: {integrity: sha512-bw24y+/1MHS4QDkons3YyHkPT9uCMoLHHgQhb+mb8NOjTYwub1CZ+K9Ngr8aO5DMrDrkqHwTzlTwFP2vS8Y/ZQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [win32]
@@ -4871,13 +4887,6 @@ packages:
'@tabler/icons@3.44.0':
resolution: {integrity: sha512-Wn0AOZG9sg0L+bjfMqq4eNhC6pQjIrk94LvvWYNYkY8KH8wC3YILRzQlrnVJc4FUeMxH/AK97QsYCX35H3LndA==}
- '@tanstack/devtools-event-client@0.4.0':
- resolution: {integrity: sha512-RPfGuk2bDZgcu9bAJodvO2lnZeHuz4/71HjZ0bGb/SPg8+lyTA+RLSKQvo7fSmPSi8/vcH3aKQ8EM9ywf1olaw==}
- engines: {node: '>=18'}
-
- '@tanstack/form-core@1.27.7':
- resolution: {integrity: sha512-nvogpyE98fhb0NDw1Bf2YaCH+L7ZIUgEpqO9TkHucDn6zg3ni521boUpv0i8HKIrmmFwDYjWZoCnrgY4HYWTkw==}
-
'@tanstack/match-sorter-utils@8.19.4':
resolution: {integrity: sha512-Wo1iKt2b9OT7d+YGhvEPD3DXvPv2etTusIMhMUoG7fbhmxcXCtIjJDEygy91Y2JFlwGyjqiBPRozme7UD8hoqg==}
engines: {node: '>=12'}
@@ -4906,12 +4915,6 @@ packages:
peerDependencies:
react: ^18 || ^19
- '@tanstack/react-store@0.8.0':
- resolution: {integrity: sha512-1vG9beLIuB7q69skxK9r5xiLN3ztzIPfSQSs0GfeqWGO2tGIyInZx0x1COhpx97RKaONSoAb8C3dxacWksm1ow==}
- peerDependencies:
- react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
- react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
-
'@tanstack/react-table@8.20.5':
resolution: {integrity: sha512-WEHopKw3znbUZ61s9i0+i9g8drmDo6asTWbrQh8Us63DAk/M0FkmIqERew6P71HI75ksZ2Pxyuf4vvKh9rAkiA==}
engines: {node: '>=12'}
@@ -4925,12 +4928,6 @@ packages:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
- '@tanstack/store@0.7.7':
- resolution: {integrity: sha512-xa6pTan1bcaqYDS9BDpSiS63qa6EoDkPN9RsRaxHuDdVDNntzq3xNwR5YKTU/V3SkSyC9T4YVOPh2zRQN0nhIQ==}
-
- '@tanstack/store@0.8.0':
- resolution: {integrity: sha512-Om+BO0YfMZe//X2z0uLF2j+75nQga6TpTJgLJQBiq85aOyZNIhkCgleNcud2KQg4k4v9Y9l+Uhru3qWMPGTOzQ==}
-
'@tanstack/table-core@8.20.5':
resolution: {integrity: sha512-P9dF7XbibHph2PFRz8gfBKEXEY/HJPOhym8CHmjF8y3q5mWpKx9xtZapXQUWCgkqvsK0R46Azuz+VaxD4Xl+Tg==}
engines: {node: '>=12'}
@@ -4938,224 +4935,224 @@ packages:
'@tanstack/virtual-core@3.11.2':
resolution: {integrity: sha512-vTtpNt7mKCiZ1pwU9hfKPhpdVO2sVzFQsxoVBGtOSHxlrRRzYr8iQ2TlwbAcRYCcEiZ9ECAM8kBzH0v2+VzfKw==}
- '@testcontainers/mysql@12.0.0':
- resolution: {integrity: sha512-ofK3bPWIkqEhwQq5hv31QO3B6AlBGF10wI2QaJDjvDiYr7jQ1WGh1n4S1Eu3xwIWyO8FYSxKjgHgkoaNPPy4Ww==}
+ '@testcontainers/mysql@12.0.1':
+ resolution: {integrity: sha512-/vC3XP+TbCC/Vvn7lATNolIFA+AcWArBIfEV/zRPGts+ntGh7t1jCMEyMF9kfOTKrQwCc0a+w2IwfAYY7b2jTQ==}
- '@testcontainers/postgresql@12.0.0':
- resolution: {integrity: sha512-mqGQHwmY+xLKFvFd3XQYaa0vDJRaJAOUfFWYbgjd4wb6hOlrK7xhszaXB7KuGCGTIJf5jvtoEB8/56oVB5s55w==}
+ '@testcontainers/postgresql@12.0.1':
+ resolution: {integrity: sha512-6SyyduUM6lTAo4UwKG1aCochP6wTe0k7/W/m5uODZQMUrV3STk6/28Km3474JLGZdw/P793BUEF2IeU7rDyoEg==}
- '@testcontainers/redis@12.0.0':
- resolution: {integrity: sha512-VRQ+saEGKyd2CgbsSG/xi74kPUO1hnEYqUdxlMMeT1gcYpSya5n78w7S0B7yuEyRIQcfQaiHiQrTPASNG+Dz5A==}
+ '@testcontainers/redis@12.0.1':
+ resolution: {integrity: sha512-x5iDR6Y2Mc+2rTJYSQlRTtBMCPt77pKxDogpqRwXgJVmWoDrWFpiFcA/dw6IEoNuyxYUg03tbS8/VzCJI5JX6A==}
- '@tiptap/core@3.23.6':
- resolution: {integrity: sha512-MRB3pHz4Oxqmcawh0cQ5iOGdY5xtNYp/1CoK7hdTLzw5K0C6/gTC2VvanB1R4INaB6EpBkxG/GiWkVirDRnuXw==}
+ '@tiptap/core@3.24.0':
+ resolution: {integrity: sha512-GTAsXAI32p4hEZgPzvUv2RPrObxamy9AFhmhG10fXSvN/cDUs8naEYVIqDV3Sh99jMwQEbTFKW1E1mcspsY6ow==}
peerDependencies:
- '@tiptap/pm': 3.23.6
+ '@tiptap/pm': 3.24.0
- '@tiptap/extension-blockquote@3.23.6':
- resolution: {integrity: sha512-2RmnqNqTltZ2k1F7IfjoDNs935Uq4rRDR7d98mqkg3OlDktcQIyBpv0t9dTay6H5bkQeZUuS8ogK2S1E8Edjug==}
+ '@tiptap/extension-blockquote@3.24.0':
+ resolution: {integrity: sha512-DgwEEJ1GbDQcT054ynxoaZGmB9apGeUklPrinq9o6xdLHpdg+bO9HCQzggdB8n21VLLglb8jfAEWsVNwh3eASQ==}
peerDependencies:
- '@tiptap/core': 3.23.6
+ '@tiptap/core': 3.24.0
- '@tiptap/extension-bold@3.23.6':
- resolution: {integrity: sha512-1LMhjnytdbbhWHSoOwnLxZAOQZWPkKyXVCNmaIk0Mhi4tLPUXptG4qKS5sVYTCveE5H6IBPFrbgBFi5dMI6krA==}
+ '@tiptap/extension-bold@3.24.0':
+ resolution: {integrity: sha512-CujogYaynasklFKHADUseuvj8X2FnWktTCCo3Hl+nlyRvBTmm5TK2aqiamg3v2P4dBh3O6a70mo8BfRJPuiR1g==}
peerDependencies:
- '@tiptap/core': 3.23.6
+ '@tiptap/core': 3.24.0
- '@tiptap/extension-bubble-menu@3.23.6':
- resolution: {integrity: sha512-Mwkyp9LkDHFbqmWRIkp63FinRxFu3ajC4qSb9t4mnHsb4kAdbNLLsGtbFg+le0SWk4CxGwAOwM7SzeJ+6UGqCA==}
+ '@tiptap/extension-bubble-menu@3.24.0':
+ resolution: {integrity: sha512-jRXD+JPu9ayvq78g8hsCxx4q/qUFtrdfIYirRSf5YUseuuUbtfrq83AsGabcygpUTefjJkMQoXNITkh6294Ggw==}
peerDependencies:
- '@tiptap/core': 3.23.6
- '@tiptap/pm': 3.23.6
+ '@tiptap/core': 3.24.0
+ '@tiptap/pm': 3.24.0
- '@tiptap/extension-bullet-list@3.23.6':
- resolution: {integrity: sha512-RMRgfXZykr/13X8UBOwvpgysVOo9KchwqMoEbvqQSj4YFfU56iIn59C8sbxiQ1sKfeltUf0wH4fPc0I4iwKqAA==}
+ '@tiptap/extension-bullet-list@3.24.0':
+ resolution: {integrity: sha512-IOpAm5c4XVVVvkOef+V9XYMVpea+3MgBpCQgn83UQRlwO9eIMwmcyxOznu7gQPQVShTEpkt4T6uK+ZN9o8meIA==}
peerDependencies:
- '@tiptap/extension-list': 3.23.6
+ '@tiptap/extension-list': 3.24.0
- '@tiptap/extension-code-block@3.23.6':
- resolution: {integrity: sha512-4kccgcn5yHThxrzsIhJny3EwfEZYIk+BjUCL4uIuzOyWvExtGhZ6JMHVCZeMhI8D1/bX1LNkkAKN5DXPzH4lXQ==}
+ '@tiptap/extension-code-block@3.24.0':
+ resolution: {integrity: sha512-NZglw4oHoH6oJ5+HvxxQCYk+wODJmsxzUpRQdsOmje08sekQH+Zt9i4UKimBhg4urpd5r+dKXTslab9a5eQ86w==}
peerDependencies:
- '@tiptap/core': 3.23.6
- '@tiptap/pm': 3.23.6
+ '@tiptap/core': 3.24.0
+ '@tiptap/pm': 3.24.0
- '@tiptap/extension-code@3.23.6':
- resolution: {integrity: sha512-KG8KXFYyLrtYvT7AZ1WGV61ofx8pDe5g9pH658MERxqQGii+Pyfc6xkz04l7XeBts/7+571UQp/0O7i/z560TA==}
+ '@tiptap/extension-code@3.24.0':
+ resolution: {integrity: sha512-MAQtrPRQ+HRmcGotWbksdIGeH1gqayFAdvi4lNGeFT7taHXP1o1XD7CQp7iYIKmg8IU4/MQ+RdetSfuC1A9edQ==}
peerDependencies:
- '@tiptap/core': 3.23.6
+ '@tiptap/core': 3.24.0
- '@tiptap/extension-color@3.23.6':
- resolution: {integrity: sha512-2WxYc+X+OGFXbq/Ak0VXODEzfK1l5wz33oTWSuhdSw44HAQM7+SSyiDnMCXDQdqi063ANqMmQmlw5WngjQ6wCA==}
+ '@tiptap/extension-color@3.24.0':
+ resolution: {integrity: sha512-gVv9zfQ9diq/sk+SVJzqadShsOtDK1XPojBZR1zXg1QxedzdwLKuRDxvnlfIc3kFIyaDP4mNrek+anednTbWBA==}
peerDependencies:
- '@tiptap/extension-text-style': 3.23.6
+ '@tiptap/extension-text-style': 3.24.0
- '@tiptap/extension-document@3.23.6':
- resolution: {integrity: sha512-XDAIgG9KcKumFM9KJWUEUhXPbFIhhl47bfy5GknareWTRKke85rcoj/oxKKO9ihLZr8JfpbXjqnS4SCm5yhYPw==}
+ '@tiptap/extension-document@3.24.0':
+ resolution: {integrity: sha512-yxgM3+yXy2XZzEwH43y2Kp8D1BkblxEWLXqo0YCoAKtxyKCcEaT8kdlf70kS7D0+VSzYU4D0iN7VdQIYHcL2mA==}
peerDependencies:
- '@tiptap/core': 3.23.6
+ '@tiptap/core': 3.24.0
- '@tiptap/extension-dropcursor@3.23.6':
- resolution: {integrity: sha512-+XWEoRKf3lXxi7Le1aOM2xU1XHwxICGpXjT3m4QaYqUgIpsq8gQEuso6kVg8DnTD7biKQs6+oIQ0o2b/gTW9WA==}
+ '@tiptap/extension-dropcursor@3.24.0':
+ resolution: {integrity: sha512-Dbv1c5LnvG3PT+yEbCNroyOeeUkHq9wcir2pbC7wri7g7d2sCi0+HvKH0MAxLwY3j5NJJSiSyG2ypMaXOAs4sg==}
peerDependencies:
- '@tiptap/extensions': 3.23.6
+ '@tiptap/extensions': 3.24.0
- '@tiptap/extension-floating-menu@3.23.6':
- resolution: {integrity: sha512-2kjuDcEq69lEcECl75xqY5MyzUSh2zcC5aLrpwP1WwhJz5bxsIFHiaps5AP6h9R4A+ZBj5b2haay2Y1wDUU3VA==}
+ '@tiptap/extension-floating-menu@3.24.0':
+ resolution: {integrity: sha512-7QEbf3mUzFAkejjQGX9f0L507oMtnOBRwHt2skUTR+9yXgudsN8zaDBSSRHLeMWGk9b7L293ZMA6zCRrZaHrfA==}
peerDependencies:
'@floating-ui/dom': ^1.0.0
- '@tiptap/core': 3.23.6
- '@tiptap/pm': 3.23.6
+ '@tiptap/core': 3.24.0
+ '@tiptap/pm': 3.24.0
- '@tiptap/extension-gapcursor@3.23.6':
- resolution: {integrity: sha512-wbKmxXsszxWacEkrHucRpSQbiKjz4fmOebD6OVyL9AcrmlbxNk8vcM3iyh/8cVeRy09XY+morM165t/u7/z4IQ==}
+ '@tiptap/extension-gapcursor@3.24.0':
+ resolution: {integrity: sha512-CzCP5/jni5RFwW9jCfBO6auh83GbaioMTpSk6tyR3sd+CbwlBcUdsJFGJkbaRdiSS9dgIyi+6hRbhjpYdHcp+w==}
peerDependencies:
- '@tiptap/extensions': 3.23.6
+ '@tiptap/extensions': 3.24.0
- '@tiptap/extension-hard-break@3.23.6':
- resolution: {integrity: sha512-KeUm+tkUfIVSX9QM9XOIhaay0Fn36sLKUo5NVYjN3uJaxFvaZXZmTlxdO85OTdgF2P5sqh9LomrIgliaFRGk4w==}
+ '@tiptap/extension-hard-break@3.24.0':
+ resolution: {integrity: sha512-T/ZEBiHQPMyTqDvXG0tiqBToNeuSemIPmNtdoGSgBN/degVl7VJZqQIrLIvOUHfjf3QkRs7TE/mcqTJsIboO/g==}
peerDependencies:
- '@tiptap/core': 3.23.6
+ '@tiptap/core': 3.24.0
- '@tiptap/extension-heading@3.23.6':
- resolution: {integrity: sha512-A/0jPhxnUh9THSZymlu0OGPZe1wdFdwHAXnRCmqvYUCwJjrG7LCC/ahzmcj1tcNzI9hgHyuYPSfev8RXYrNu/w==}
+ '@tiptap/extension-heading@3.24.0':
+ resolution: {integrity: sha512-GCSgapIzQPqEGNcVGE0/Pcjg5wITMLYJlrS3GGVw7BPmECJwgexcoOsEwkxtzJnXT/HpFXbvOFW43sM0KeHSjg==}
peerDependencies:
- '@tiptap/core': 3.23.6
+ '@tiptap/core': 3.24.0
- '@tiptap/extension-highlight@3.23.6':
- resolution: {integrity: sha512-GWv6R4iZzVrNeB33qtJDBt/BfVjuc5S/sUF/VDFAtK8h86bg4cEm1tGLEsui4T3+vij/t+P+eoGhkzF6NGogdg==}
+ '@tiptap/extension-highlight@3.24.0':
+ resolution: {integrity: sha512-+Vx6YKSP+lzCBiTbgQOpR2rIOzL8Ns6UnOBtW4O6tYc7TMTexycaG66RWxXMpAD6QkamwLwX3H5TTYviWtU9RQ==}
peerDependencies:
- '@tiptap/core': 3.23.6
+ '@tiptap/core': 3.24.0
- '@tiptap/extension-horizontal-rule@3.23.6':
- resolution: {integrity: sha512-hEUlz4H+I64r+TH6LCuNCRgO7JTHncXGmx9+WbU69EOfY8O0ZurcgeJc8HeiAKL+r9YuC1e5YHfFxgCaaC0jlg==}
+ '@tiptap/extension-horizontal-rule@3.24.0':
+ resolution: {integrity: sha512-DFzWJTrb23x+qssLLs85vEyho8ItUGp3RY9XUsVTIAGZn5IsoUw8wMsvIBlH1ux4Ch7gLchtcD6kpTdMdrL9kw==}
peerDependencies:
- '@tiptap/core': 3.23.6
- '@tiptap/pm': 3.23.6
+ '@tiptap/core': 3.24.0
+ '@tiptap/pm': 3.24.0
- '@tiptap/extension-image@3.23.6':
- resolution: {integrity: sha512-vvNGxArvD2dW+XvV0KdYovRVUzCy8QVNulc2r5pV7umnG1E6cCmMkiHiif8J2ePJu2KtysAvJQe0iF+UqueGMw==}
+ '@tiptap/extension-image@3.24.0':
+ resolution: {integrity: sha512-mH+bvsX2cPKuZzV7YMQi4FV2YbDP+Kmq36bY+Bwi/x4mYUc8u0cjQxcu8RzLO7GtsgUJPxGMwfkQxmDqXFLZvw==}
peerDependencies:
- '@tiptap/core': 3.23.6
+ '@tiptap/core': 3.24.0
- '@tiptap/extension-italic@3.23.6':
- resolution: {integrity: sha512-wol5KdwCPAvpiYhH9PLlvO8ZnJHwZtIboVevrfOGgBcKlXRA3dedR4OAMXHnUtkkzu9KtliLg1+TYzEx4JZG9Q==}
+ '@tiptap/extension-italic@3.24.0':
+ resolution: {integrity: sha512-mf3cbNlbMPUNj3IyUkIke+o3ZpOUrtVeY5Yqs5IM/VhkUUh/PdIzqw74VuqEAJ0Z4oZ6nNDHeYLrl3Be1j99lQ==}
peerDependencies:
- '@tiptap/core': 3.23.6
+ '@tiptap/core': 3.24.0
- '@tiptap/extension-link@3.23.6':
- resolution: {integrity: sha512-KNZz7z7P2/qbQsx5bPAbSPjrKDg1VHsedGlLHJCr8U2VRD5VgmDLkMpkouP1CsDg15qgyUKv/nDib5KgPpLNWA==}
+ '@tiptap/extension-link@3.24.0':
+ resolution: {integrity: sha512-MwMoNGG2mL5XGFV1tEGunBRglwsIbW+ZOB2QnKiv+Mcbi2JCWMrorndJZBqpVPR5nM+Bef2KnpchEJmYlQLvKQ==}
peerDependencies:
- '@tiptap/core': 3.23.6
- '@tiptap/pm': 3.23.6
+ '@tiptap/core': 3.24.0
+ '@tiptap/pm': 3.24.0
- '@tiptap/extension-list-item@3.23.6':
- resolution: {integrity: sha512-3zzyhdkUWcHVpXuvy6KiIwjh29rbH6gEDEqPQqHLrl1XGnO9pnShC7pSHctlCDjmcx3O4n9cd4QMtVBlUerbiA==}
+ '@tiptap/extension-list-item@3.24.0':
+ resolution: {integrity: sha512-zl/U3viJiV9OzkKM37AHIUN1af1TSLrcbHUUoNLkfJ33Nq+NlpaXpCVK0rKRqiLFJf7zk/a5KWG5CrOy9TxjKA==}
peerDependencies:
- '@tiptap/extension-list': 3.23.6
+ '@tiptap/extension-list': 3.24.0
- '@tiptap/extension-list-keymap@3.23.6':
- resolution: {integrity: sha512-x8bPcLViGzg/RAmQM/XtmfqIwQ/Pv9Q8mkd+OgfUiTqjeJqKwVQmiqbLFNa7zw81+H61M+HDU+qGAaQ3vRIMjw==}
+ '@tiptap/extension-list-keymap@3.24.0':
+ resolution: {integrity: sha512-69fKcrngYGEKWNn4R5oLwl0YuV3FY4kufEValVcjnihUmqJTE1vx+fwctYoTsOGnIuNGpUIQ7f9YDD/0w34qBw==}
peerDependencies:
- '@tiptap/extension-list': 3.23.6
+ '@tiptap/extension-list': 3.24.0
- '@tiptap/extension-list@3.23.6':
- resolution: {integrity: sha512-z6vj9+Qht2sjdQkyyHcUpsC/yCIZqTrQiyHDhs/HGKrfvoANyAZGpqdNeKf1wSyjIso+27tQuIH5NDfk8ygyNw==}
+ '@tiptap/extension-list@3.24.0':
+ resolution: {integrity: sha512-GcxDVMMmDGj7OFTBrV7JpVgr5wxlr2vmjwH7U8QxZX7OJI5vrsMYl/U6KRTvUpG8wP+Zmo5jRlLM+BbL+a/W3g==}
peerDependencies:
- '@tiptap/core': 3.23.6
- '@tiptap/pm': 3.23.6
+ '@tiptap/core': 3.24.0
+ '@tiptap/pm': 3.24.0
- '@tiptap/extension-ordered-list@3.23.6':
- resolution: {integrity: sha512-1m/wWB/ZtXcmG2vNdiUkCqsOgqv5vBjCv/mVaHhF9OvV+zQS8YDjoWE7zEuT/GgELdT77Xq8lHrn4nCDudB3/A==}
+ '@tiptap/extension-ordered-list@3.24.0':
+ resolution: {integrity: sha512-buRa6bmBDw0TztH+rAcusIye14DiLDS+yGheo6GiNCTD7kKJnksXagBdxvip3jhW5sx7gyAKvoBmvGSg1BbsGA==}
peerDependencies:
- '@tiptap/extension-list': 3.23.6
+ '@tiptap/extension-list': 3.24.0
- '@tiptap/extension-paragraph@3.23.6':
- resolution: {integrity: sha512-+7m58LUSncodjrIyXks4RZ3tLNYrvgT77wRR4l3HnM5OABY3GDsDTqi7c1t1yI29NVOSk/DUacqy6UwYAj1DGg==}
+ '@tiptap/extension-paragraph@3.24.0':
+ resolution: {integrity: sha512-wD06aB6hO7LgcrlhGiw7I64k2tus9kNoICX5R+UecBSB1DVJdzKvXoXL2kPNv4DqYvljHdkIeK/OpuOTQd6MJA==}
peerDependencies:
- '@tiptap/core': 3.23.6
+ '@tiptap/core': 3.24.0
- '@tiptap/extension-placeholder@3.23.6':
- resolution: {integrity: sha512-8I6b2aevF74aLgymKMxbDxSLxWA2y+2dh0zZDeI8sRZ2m6WHHes+Kyuuwkq1HIPcR+ZLpbec74cmf6lcL/yvqQ==}
+ '@tiptap/extension-placeholder@3.24.0':
+ resolution: {integrity: sha512-3jfYYCIuwMADhvZ92vB6c80YiTmgTSFR23JqyLps8qkQtV59Va5CBYpwJtSs1+VrbCVnNxhZBHhVXusZO3uRkA==}
peerDependencies:
- '@tiptap/extensions': 3.23.6
+ '@tiptap/extensions': 3.24.0
- '@tiptap/extension-strike@3.23.6':
- resolution: {integrity: sha512-oF7FEZ37f15aCe5kPgzGDYf/m+hr7VdQ/Ko/Hds/UM9pX7AG1fdtmRrl6wqkRqDM/incZaC/AQR2/Dpo2VCNGQ==}
+ '@tiptap/extension-strike@3.24.0':
+ resolution: {integrity: sha512-sfN1iQs6Fdlorrfe8wipDkTPwu/Egx3s2fkY7TAWusTGFHwlovuRUGFKqCL9dI4N3u6uqUMpEuWmQNgv+aQGjQ==}
peerDependencies:
- '@tiptap/core': 3.23.6
+ '@tiptap/core': 3.24.0
- '@tiptap/extension-table-cell@3.23.6':
- resolution: {integrity: sha512-hS9TmmvRlT9/ikT+0ukACS+hmJuii4zQaH47cg3oJkz/Fv7O7tL7GZniKtK6l2OUZGPhY+4SV2RkDB6bD7DXfw==}
+ '@tiptap/extension-table-cell@3.24.0':
+ resolution: {integrity: sha512-jg62iVc1L8tOycSuwZ5i82EGKGuvelCIkpFX7wvHMl9EiPX9fv70enmb4ua9DyLgRtYg/MNrGOdUATRpxWsFRA==}
peerDependencies:
- '@tiptap/extension-table': 3.23.6
+ '@tiptap/extension-table': 3.24.0
- '@tiptap/extension-table-header@3.23.6':
- resolution: {integrity: sha512-D6o0a1cJXUU0xWakainBFGPnGHinQkPcdu1YqGd/PoFANY38lnuZt/NW2O/OLfLXu5LXDRfpqF1+dsKww27dUA==}
+ '@tiptap/extension-table-header@3.24.0':
+ resolution: {integrity: sha512-z1TGVoBD5lVX48hpKcrQY5vETB8aCR61Enk5wGl45zNO/sMEMNxYfDKTkMydyOFUWfGqvOPnjDHJqJuT36/19w==}
peerDependencies:
- '@tiptap/extension-table': 3.23.6
+ '@tiptap/extension-table': 3.24.0
- '@tiptap/extension-table-row@3.23.6':
- resolution: {integrity: sha512-OauWVzkyRQg0rKOqM/a3PuKPc1S7YXMb1LRN7Nh8Ytvglvd7GFRTbl1lVqdZRaz4Jzopag4PQnriIZfMPUpxWw==}
+ '@tiptap/extension-table-row@3.24.0':
+ resolution: {integrity: sha512-kASEYMWZF9ZqO6DRGz0LiD11c3LPfzOlFiJQVDrOyLP7xkJpHzIAKH0K+8u4aohOziDLn8nz+8I+3xjRS46oGg==}
peerDependencies:
- '@tiptap/extension-table': 3.23.6
+ '@tiptap/extension-table': 3.24.0
- '@tiptap/extension-table@3.23.6':
- resolution: {integrity: sha512-XbhZXjhsS6AP7ThoZxjAnNs+NiR81YRori25l6E+ORqB7quiPkIXOAi5h4AIpkn/CYIqze6ere11lWsYpDjtaQ==}
+ '@tiptap/extension-table@3.24.0':
+ resolution: {integrity: sha512-lr5elob3uJnB+ltgqPDEeVQmIPRx6JoS0I6z93tOgKsI2mIsaw5ErghteeiCTpExdyax7aWR0fn5pZzLVDQL8A==}
peerDependencies:
- '@tiptap/core': 3.23.6
- '@tiptap/pm': 3.23.6
+ '@tiptap/core': 3.24.0
+ '@tiptap/pm': 3.24.0
- '@tiptap/extension-task-item@3.23.6':
- resolution: {integrity: sha512-PIHW+3KZCtEmtC7z34rfkusbC42HYr7uw1h5xNvrazQwJGWhKo1UmI0Yea5vyJNKPrTLPCMEm/mgMrj8nNAZfA==}
+ '@tiptap/extension-task-item@3.24.0':
+ resolution: {integrity: sha512-2VtX6VOp4SJk5E/VHeSC2Acfbge6h69TUBhF7hFebbiCDyB73xCaPKcuClIyzOlWJgpWz2ZPXwVB+Y3meXw+yw==}
peerDependencies:
- '@tiptap/extension-list': 3.23.6
+ '@tiptap/extension-list': 3.24.0
- '@tiptap/extension-task-list@3.23.6':
- resolution: {integrity: sha512-XNuYbVV7F0TBpxHqqEFvrxwGbmTq01qJrZA6qwbQopOsw5wOsCy4XQsCkeZ8BGkakORCEWGSMjPyVOtIKq2jGA==}
+ '@tiptap/extension-task-list@3.24.0':
+ resolution: {integrity: sha512-EIbB/WQ7L92WfaY5AF9upH93nIgBHjGt5zwPYoX+ZTpBncebr1AEDF7rc+LHNuSa1g1ToOKbFB88ep02IIXOjA==}
peerDependencies:
- '@tiptap/extension-list': 3.23.6
+ '@tiptap/extension-list': 3.24.0
- '@tiptap/extension-text-align@3.23.6':
- resolution: {integrity: sha512-pE+gDmvnrSMWHADDnDSzaO7YUi9kOywQkyrqZ9bEPLddZred7ICoJ4NtD2DqLjDSur+HijaJuByResWb78c6FA==}
+ '@tiptap/extension-text-align@3.24.0':
+ resolution: {integrity: sha512-WKFtYXGthtkUc+Cwy2fItSr+9FKwLZjkJVAY1GhkRdcq35qTuVhkb4Q4wR2Rhkb6QRqtlxF1NDuTf2vxiQmfBQ==}
peerDependencies:
- '@tiptap/core': 3.23.6
+ '@tiptap/core': 3.24.0
- '@tiptap/extension-text-style@3.23.6':
- resolution: {integrity: sha512-Auasgj469wkQ5ip+Zi2gaRzvqxx9qKG58+1mkT8yPE3QAGnOIg/AaKyQ7pTV77UyL3FHvLnU+KsWCad+qcobww==}
+ '@tiptap/extension-text-style@3.24.0':
+ resolution: {integrity: sha512-1Hy+5tFEAsnoLhZ/eqmza4USvFHwMA8haeAdCGlwTeshBrt+nUKTrEsRHidF60cGsRwlTcuqxSkjT94dULgp5Q==}
peerDependencies:
- '@tiptap/core': 3.23.6
+ '@tiptap/core': 3.24.0
- '@tiptap/extension-text@3.23.6':
- resolution: {integrity: sha512-ipoC2TkIAIOTiF5ByiGgvQB1DqDyfP90wrUB3mohBcgvp7lQnwHszCDGv8dNnmcUek8uXV/uoLu2VXeVQlxjPA==}
+ '@tiptap/extension-text@3.24.0':
+ resolution: {integrity: sha512-Im7keLPEihxm3+LyF+drYCoaOY5hlq35lvHAp/el6M8pJ/scts88HrYpdR1Yc4BtpZBIhfHSyWgPaupI4qwdeg==}
peerDependencies:
- '@tiptap/core': 3.23.6
+ '@tiptap/core': 3.24.0
- '@tiptap/extension-underline@3.23.6':
- resolution: {integrity: sha512-P55wGIZGYTVH92Fq0cgI4/O9AhLCaJC3hhxg15RSERP5/YegM9eJHDK/GQ1EE/DvYA+xpYGOV6agKwAUqfA/Iw==}
+ '@tiptap/extension-underline@3.24.0':
+ resolution: {integrity: sha512-D4W4X3UMq9dLVIOfPB9+UodQ4eAJ8yDcm8qFWAwq0a15YWH6bnwulCuIdV+U5dEG+yaRxN8haB9GrrID9jmrSA==}
peerDependencies:
- '@tiptap/core': 3.23.6
+ '@tiptap/core': 3.24.0
- '@tiptap/extensions@3.23.6':
- resolution: {integrity: sha512-X09/Db1teB+ifXzDGVVFmOeQRx7wTAayE9/280spxpsHkHZvJ5bHRvWIzUzviMIjbBz+NPDIKYPK7gMfh9iaig==}
+ '@tiptap/extensions@3.24.0':
+ resolution: {integrity: sha512-z6gRYzy2ucJp07OQ0F2W07NxyhMTxPYH1ia2eGiQkWax1i56oExpjMsDHP8THWlg8Tb7NnbfKpkfh881EsmofA==}
peerDependencies:
- '@tiptap/core': 3.23.6
- '@tiptap/pm': 3.23.6
+ '@tiptap/core': 3.24.0
+ '@tiptap/pm': 3.24.0
- '@tiptap/pm@3.23.6':
- resolution: {integrity: sha512-in5CaMaWlJcH2A1q6GJKFtrodE8WLS3M9tIi/f89jPmIVHJShpodC0KZDNyJkrVBQomYk0DEh86Utm6ASXzQww==}
+ '@tiptap/pm@3.24.0':
+ resolution: {integrity: sha512-QQP/78ryOZDN99gNBV7dgh69/8AYaOYQYFklq/iR+ZRFaaL3+qqHFvPVJapGkzPdymBgNJ34xjFM8n5pJ4QmMg==}
- '@tiptap/react@3.23.6':
- resolution: {integrity: sha512-Tw9KZkYqFMk3vaJAEQKqEYIO/iq3cSJe7OUEGBul4k4GaMQeLItLf5EYhUd0GIPXci1WVVPNntKJsHfX25M37w==}
+ '@tiptap/react@3.24.0':
+ resolution: {integrity: sha512-KxnrlQbzOgA02EMsfuGGHtNhfkJQGqVlQttmQctI9DOl/F3gcaRqg+wNTBY1Fof8yDaZ8Z1LL1F0C05W0o3vUw==}
peerDependencies:
- '@tiptap/core': 3.23.6
- '@tiptap/pm': 3.23.6
+ '@tiptap/core': 3.24.0
+ '@tiptap/pm': 3.24.0
'@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0
'@types/react-dom': ^17.0.0 || ^18.0.0 || ^19.0.0
react: ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0
- '@tiptap/starter-kit@3.23.6':
- resolution: {integrity: sha512-gykwtGWrnWCmtql1hid3opac/KV8zQvOAnu3bTqIqcHrn1FusbUwKmNzavSbfGvcktHM3hFjb35W48JyVLyu/A==}
+ '@tiptap/starter-kit@3.24.0':
+ resolution: {integrity: sha512-Ef4PCP96vcY2GonXN9J0M8iC6zvxPTmQlL/QZiCwuYqqnH/hNpYIjNSQdTndiDpxRKofa32Sr2HWktgEnL32Bg==}
'@tree-sitter-grammars/tree-sitter-yaml@0.7.1':
resolution: {integrity: sha512-AynBwkIoQCTgjDR33bDUp9Mqq+YTco0is3n5hRApMqG9of/6A4eQsfC1/uSEeHSUyMQSYawcAWamsexnVpIP4Q==}
@@ -5215,37 +5212,37 @@ packages:
react: '>=18.2.0'
typescript: '>=5.7.2'
- '@turbo/darwin-64@2.9.14':
- resolution: {integrity: sha512-t7QiPflaEyBE4oayeZtSmu4mEfjgIrcNlNNl1z1dmIVPqEdtA7+CfTf8d7KXsOGPh6aNgWjKxyvQg9uGfDQF+A==}
+ '@turbo/darwin-64@2.9.16':
+ resolution: {integrity: sha512-jLjApWTSNd7JZ5JaLYfelW1ytnGQOvB7ivl+2RD1xQvJTbi8I9gBjzcga7tDZVPyaxpl10YTfJt3BrYXR18KDw==}
cpu: [x64]
os: [darwin]
- '@turbo/darwin-arm64@2.9.14':
- resolution: {integrity: sha512-d23147mC9BsCPA9mJ0h/ubcpbRgcJBXbcG3+Vq7YLhjz3IXuvQsJ1UXH8f4MD76ZjJ4m/E4aRdJV+MW88CDfbw==}
+ '@turbo/darwin-arm64@2.9.16':
+ resolution: {integrity: sha512-YPgrn+5HIGzrx0O2a631SV4MBQUe4W/DafMFUuBVgaU32PW9/OTT0ehviF0QSxTXuRJlHvW2eUTemddF5/spmw==}
cpu: [arm64]
os: [darwin]
- '@turbo/gen@2.9.14':
- resolution: {integrity: sha512-TkvUzfbJj2zB0WaybYd/hzADLqq97ru0OQCiQ1DDnul5spSTq5I8If21K3vUAjKIWAQbCjDqZIPrS1IPG5bLyA==}
+ '@turbo/gen@2.9.16':
+ resolution: {integrity: sha512-nvEBt1FUqA7zO821Yw/GuggAG8JeT0J2en+vwvuSNqHa0df+a2zhzHF2vx/oguWQm1HCjgx87VIJWzMOr2grYA==}
hasBin: true
- '@turbo/linux-64@2.9.14':
- resolution: {integrity: sha512-P3ZKB5tuUDdDQWuAsACGUR1qv9W7BNWxdxqVJ0kZNuNNPRaVYTPPikLcp79+GiEcW3npsR+KyP38lnQiBc5aSA==}
+ '@turbo/linux-64@2.9.16':
+ resolution: {integrity: sha512-vAEf1H6l26lTpl9FJ/peQo1NUB8RC0sbEJJz5mPcUhHA2bPDup2x3CZPgo/bH8S4cUcBLm4FN3UHd5iUO2RAew==}
cpu: [x64]
os: [linux]
- '@turbo/linux-arm64@2.9.14':
- resolution: {integrity: sha512-ZRTlzcUMrrPv9ZuDzRF9n60Ym13bKeG9jDB8WjxyLhWNzV+AJQN+zdpIk3NJYf2zQsGUm1mNar2P0elRzLw25g==}
+ '@turbo/linux-arm64@2.9.16':
+ resolution: {integrity: sha512-xDBLR2PZg4BrQOchfG6svgpv5FCNJ2TOtT2psLdEJcdKo1BH+pnPs9Xj6pvUjgfkHbuvBOfeE4R6tvxMoQKDHQ==}
cpu: [arm64]
os: [linux]
- '@turbo/windows-64@2.9.14':
- resolution: {integrity: sha512-exanwN6sIduZwykYeiTQj8kCmOhazP5WOz3bvXMcYtjhL6Z3iRWLewKrXCBq0bqwSP3iBMb/AerRCnHI4lx46A==}
+ '@turbo/windows-64@2.9.16':
+ resolution: {integrity: sha512-NBAJnaUiGdgkSzQwUIdOvkCkcpTSu58G/sBGa0mvBtzfvFOOgrQwepKOOQ8cp6sWM6OcKDNFj2p1dsZA1OWjPg==}
cpu: [x64]
os: [win32]
- '@turbo/windows-arm64@2.9.14':
- resolution: {integrity: sha512-fVdCsnmYoKICsycbWuuGp6Jvi51/3G/UluFWuAUCvR8PIW5IJkAk5BM9UF8PSm0Q2IphWHFZjYEgjHsh3B9y/g==}
+ '@turbo/windows-arm64@2.9.16':
+ resolution: {integrity: sha512-Y7SJppD0Z8wjO3Ec0ZGd9KQ4Yv0BMnA8CIowj5Vp+OEVsosXDG2weK6/t1RRLfJmc2Ozrnd6y4DOgQys+mn3WQ==}
cpu: [arm64]
os: [win32]
@@ -6010,8 +6007,8 @@ packages:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'}
- cluster-key-slot@1.1.2:
- resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==}
+ cluster-key-slot@1.1.1:
+ resolution: {integrity: sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw==}
engines: {node: '>=0.10.0'}
co-body@6.2.0:
@@ -6236,8 +6233,8 @@ packages:
resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==}
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
- dayjs@1.11.20:
- resolution: {integrity: sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==}
+ dayjs@1.11.21:
+ resolution: {integrity: sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==}
debug@4.4.3:
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
@@ -6342,6 +6339,9 @@ packages:
dompurify@3.4.5:
resolution: {integrity: sha512-OrwIBKsdNSVEeubdJ1HBv/wNENRM9ytAVCv7YXt//A3vPdVMNuACRqK9mXCGCBW2ln7BT/A4X0jXHo2Gu89miA==}
+ dompurify@3.4.7:
+ resolution: {integrity: sha512-2jBxDJY4RR06tQNy4w5FlFH7kfxsQZlufd0sbv+chfHCxeJwrFw2baUDsSwvBISD4K4RDbd0PTfy3uNXsR6siA==}
+
dot-prop@5.3.0:
resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==}
engines: {node: '>=8'}
@@ -7026,8 +7026,8 @@ packages:
resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==}
engines: {node: '>=0.10.0'}
- icu-minify@4.12.0:
- resolution: {integrity: sha512-zDmM05uav3t3+kxSfRrNlmyXOdj2b+uHA+p04CG32eJabtaHbugXujuL+YfRkwP9joAnf0Uh+RMGCKD5NLa5rQ==}
+ icu-minify@4.13.0:
+ resolution: {integrity: sha512-SIFMeUHZJjzS5RvIGvybKvWoHjDm9cGVEs2EpJ8PmywOdJLWyblPm7TdPLLoUtkJtwQD7iGhl2WMptZ+N0on+w==}
ieee754@1.2.1:
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
@@ -7091,8 +7091,8 @@ packages:
invariant@2.2.4:
resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==}
- ioredis@5.10.1:
- resolution: {integrity: sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA==}
+ ioredis@5.11.0:
+ resolution: {integrity: sha512-EZBErytyVovD8f6pDfG3Kb37N6Y3lmDA9NNj+4+IP13CzzHGeX+OyeRM2Um13khRzoBSzzL+5lVnCX8V2RLeMg==}
engines: {node: '>=12.22.0'}
ip-address@9.0.5:
@@ -7201,8 +7201,8 @@ packages:
resolution: {integrity: sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==}
engines: {node: '>=18'}
- isomorphic-dompurify@3.14.0:
- resolution: {integrity: sha512-64W8/lsfqgaDWfEkvrIVk8FdIk29Mya0Fp39excQEdlcLUPg1Cn7CtCYe6CtPbFW90JpEKTXG0QQtIUNENJ7sw==}
+ isomorphic-dompurify@3.15.0:
+ resolution: {integrity: sha512-9ZtkbQ8+SgNf6LuDAdu9bq23dVXMIGNM8ZYnyl2MufyZiSD5dqAUJcyjtYZz7B80HuPpEn/f0NCS6zKvavHtfA==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24.0.0}
isomorphic-ws@5.0.0:
@@ -7391,15 +7391,9 @@ packages:
lodash.debounce@4.0.8:
resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
- lodash.defaults@4.2.0:
- resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==}
-
lodash.escaperegexp@4.1.2:
resolution: {integrity: sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==}
- lodash.isarguments@3.1.0:
- resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==}
-
lodash.isequal@4.5.0:
resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==}
deprecated: This package is deprecated. Use require('node:util').isDeepStrictEqual instead.
@@ -7460,18 +7454,18 @@ packages:
resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==}
engines: {node: '>=10'}
- mantine-react-table@2.0.0-beta.9:
- resolution: {integrity: sha512-ZdfcwebWaPERoDvAuk43VYcBCzamohARVclnbuepT0PHZ0wRcDPMBR+zgaocL+pFy8EXUGwvWTOKNh25ITpjNQ==}
+ mantine-react-table-open@9.0.3:
+ resolution: {integrity: sha512-HsrBU4KZluj96FPvvH0wcLzog0m5LIMdwB/59oj6BpftLGEibiLxX00G+tDM06PL2jXuiRxiGKPJ4oQOV+VK8w==}
engines: {node: '>=16'}
peerDependencies:
- '@mantine/core': ^7.9
- '@mantine/dates': ^7.9
- '@mantine/hooks': ^7.9
+ '@mantine/core': ^9.0.0
+ '@mantine/dates': ^9.0.0
+ '@mantine/hooks': ^9.0.0
'@tabler/icons-react': '>=2.23.0'
clsx: '>=2'
dayjs: '>=1.11'
- react: '>=18.0'
- react-dom: '>=18.0'
+ react: '>=19.2'
+ react-dom: '>=19.2'
maria2@0.4.1:
resolution: {integrity: sha512-3lr8UeB+R+BN9c7eOBkeH1GlHf7+0Kzt0pcEgsT9xoXIA5gfXAzjd1niYiYDsvb62v5rLPqCqDiyYxtmmhGJ3g==}
@@ -7694,8 +7688,8 @@ packages:
engines: {node: '>=8', npm: '>=5'}
hasBin: true
- mysql2@3.22.3:
- resolution: {integrity: sha512-uWWxvZSRvRhtBdh2CdcuK83YcOfPdmEeEYB069bAmPnV93QApDGVPuvCQOLjlh7tYHEWdgQPrn6kosDxHBVLkA==}
+ mysql2@3.22.4:
+ resolution: {integrity: sha512-CtXYlmL7ZamiYKbmqkamQHWJROUHSfm+f3kByzGfknw7kW51mcB2ouMUqYq1XfYxbXmnWo6RhPydx6OCqdgcmQ==}
engines: {node: '>= 8.0'}
peerDependencies:
'@types/node': '>= 8'
@@ -7748,11 +7742,11 @@ packages:
nodemailer:
optional: true
- next-intl-swc-plugin-extractor@4.12.0:
- resolution: {integrity: sha512-jUxVEu1Nryjt4YgaDktSys7ioOgQfcNPF/SF2dbPNxbVb6U+P1INRgHeCVN+EC59H2rnTFIQwbddmOCrUWFr3g==}
+ next-intl-swc-plugin-extractor@4.13.0:
+ resolution: {integrity: sha512-6S/fJI0KXvLCL8nhBo9P8eGaJPzmwJBTCzX0NaUIj0VyU8U89d//T+vjMLdNIXl5MlLaYH7B9MbAjb8Mvu+tqQ==}
- next-intl@4.12.0:
- resolution: {integrity: sha512-v8KpppWG0yLLlChJ3Of6uoPew9LeRDBAtY6vpJmF7YJmBZlHEzzoEL4w1g1dAU+VleEPNoXNm9hg1eEsKWV5hw==}
+ next-intl@4.13.0:
+ resolution: {integrity: sha512-OvNq2v5XLx4EkQOsAhVE9g+6zdb83XHusADCXXtIW4LILYnjEVaeINdr1lkVWKSjzwNUiMSlH5N4K0OQTRiv6A==}
peerDependencies:
next: ^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0
@@ -7987,25 +7981,31 @@ packages:
outvariant@1.4.3:
resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==}
- oxfmt@0.51.0:
- resolution: {integrity: sha512-l/AoAnaEOV7Q5/Z9kHOMDehVJnCgYN7wRoooWCTUMBMi16BJhLZqd9cmCnwcVFfVlzkt53zK2KLPFNp8vSsoDg==}
+ oxfmt@0.52.0:
+ resolution: {integrity: sha512-nJlYM35F64zTDMecCNhoHNkf+D/eHv7xcjj9XDSj+bFAVtN93m7v8DQMdHd6nDG6Akf/kEYYHmDUBs2Dz27Sug==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
peerDependencies:
svelte: ^5.0.0
+ vite-plus: '*'
peerDependenciesMeta:
svelte:
optional: true
+ vite-plus:
+ optional: true
- oxlint@1.66.0:
- resolution: {integrity: sha512-N4LLxYLd94KEBqXDMDM5f+2PUpItTjDLreXe2Gn5KhjhCK4Qp2YUXaBi8Yu325ryOgKwt22m45fpD7nPOn69Yw==}
+ oxlint@1.67.0:
+ resolution: {integrity: sha512-blwwaHPdoH8piQ5/z0KHeoHFR7FZgl12WluKJfu4qFLPkZl6mK04PkLE45Fw1NxfBRSlh40Gu7MkxHUw++ociQ==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
peerDependencies:
oxlint-tsgolint: '>=0.22.1'
+ vite-plus: '*'
peerDependenciesMeta:
oxlint-tsgolint:
optional: true
+ vite-plus:
+ optional: true
p-each-series@3.0.0:
resolution: {integrity: sha512-lastgtAdoH9YaLyDa5i5z64q+kzOcQHsQ5SsZJD3q0VEyI8mq872S3geuNbRUQLVAE9siMfgKrpj7MloKFHruw==}
@@ -8325,6 +8325,9 @@ packages:
prosemirror-history@1.4.1:
resolution: {integrity: sha512-2JZD8z2JviJrboD9cPuX/Sv/1ChFng+xh2tChQ2X4bB2HeK+rra/bmJ3xGntCcjhOqIzSDG6Id7e8RJ9QPXLEQ==}
+ prosemirror-inputrules@1.5.1:
+ resolution: {integrity: sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==}
+
prosemirror-keymap@1.2.2:
resolution: {integrity: sha512-EAlXoksqC6Vbocqc0GtzCruZEzYgrn+iiGnNjsJsH4mrnIGex4qbLdWWNza3AW5W36ZRrlBID0eM6bdKH4OStQ==}
@@ -8439,8 +8442,8 @@ packages:
peerDependencies:
react: '>= 16.8 || 18.0.0'
- react-error-boundary@6.1.1:
- resolution: {integrity: sha512-BrYwPOdXi5mqkk5lw+Uvt0ThHx32rCt3BkukS4X23A2AIWDPSGX6iaWTc0y9TU/mHDA/6qOSGel+B2ERkOvD1w==}
+ react-error-boundary@6.1.2:
+ resolution: {integrity: sha512-3DpCr5HVdZ0caUjYE/kIHBEJN0mNP3ZCgf16c48uJ5TbWjorKVp+YG8W3XqlJ7vJAVNw6wNIImyPXmFydwmyng==}
peerDependencies:
react: ^18.0.0 || ^19.0.0
@@ -9157,8 +9160,8 @@ packages:
engines: {node: '>=10'}
hasBin: true
- testcontainers@12.0.0:
- resolution: {integrity: sha512-/PdRvFvuHPwX126HR7RO0cEgLD3Nr8sWZyWSv54ei92TT79BubUkOCU5uwTc8ufTsTGQf0v6nyvZJVVVyR9Uqw==}
+ testcontainers@12.0.1:
+ resolution: {integrity: sha512-EMjjfMNJf3HlL7V3elkxqKUO1r3CtqNBTdmKGwwma/lOtUGfoWvFJ0WQ/KQf1DHEMnRjLWzW4cXbv/Tndsbcbw==}
text-decoder@1.2.0:
resolution: {integrity: sha512-n1yg1mOj9DNpk3NeZOx7T6jchTbyJS3i3cucbNN6FcdPriMZx7NsgrGpWWdWZZGxD7ES1XB+3uoqHMgOKaN+fg==}
@@ -9224,8 +9227,8 @@ packages:
resolution: {integrity: sha512-M9ZQBPp6FyqhMcl233vHYyYRkxXOA1SKGlnq13S0mJdUhRSwr2w6I8rlchPL73wBwRlyIZpFvpu2VcdSMWLYXw==}
hasBin: true
- tmp@0.2.5:
- resolution: {integrity: sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==}
+ tmp@0.2.7:
+ resolution: {integrity: sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==}
engines: {node: '>=14.14'}
to-buffer@1.2.1:
@@ -9349,8 +9352,8 @@ packages:
resolution: {integrity: sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==}
engines: {node: '>=0.6.11 <=0.7.0 || >=0.7.3'}
- turbo@2.9.14:
- resolution: {integrity: sha512-BQqXRr4UoWI3UPFrtznCLykYHxwxWh53iCB57x092jPMjIlW1wnm3N895g5irpiXmnxUhREBB0n6+y8BHhs4nw==}
+ turbo@2.9.16:
+ resolution: {integrity: sha512-NqgRQy6j6dPYcdSdv0q1g9QsZg7SWg87RERM8otw/1AtKU2yTFVClOM7cbwKzOonZr/Ek1blTBucw64L9H0Bwg==}
hasBin: true
tweetnacl@0.14.5:
@@ -9511,8 +9514,8 @@ packages:
peerDependencies:
react: '>=16.13'
- use-intl@4.12.0:
- resolution: {integrity: sha512-r+qVb7UI1+kiOhjYsmsNUCY+jrnjVopwGeFlmMyQj4YInlwZzgMeMSv9n8MqnWWy77HL5BVM8K2WgX50SbtcpA==}
+ use-intl@4.13.0:
+ resolution: {integrity: sha512-fAFDrWaASxlhXOipcOyb5VDD+YONqj6+8O8EcG/J7RBoOUF3A8YahRWLN+mBxYMrlMQB8N6Voqk5X+YC+HSL0A==}
peerDependencies:
react: ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0
@@ -10044,6 +10047,8 @@ snapshots:
'@babel/runtime@7.29.2': {}
+ '@babel/runtime@7.29.7': {}
+
'@babel/template@7.28.6':
dependencies:
'@babel/code-frame': 7.29.0
@@ -10120,7 +10125,7 @@ snapshots:
rfc4648: 1.5.4
uint8array-extras: 1.5.0
- '@ctrl/qbittorrent@9.12.1':
+ '@ctrl/qbittorrent@9.12.2':
dependencies:
'@ctrl/magnet-link': 4.3.0
'@ctrl/shared-torrent': 6.5.1(ofetch@1.5.1)
@@ -10473,21 +10478,10 @@ snapshots:
'@fastify/forwarded': 3.0.1
ipaddr.js: 2.3.0
- '@floating-ui/core@1.7.3':
- dependencies:
- '@floating-ui/utils': 0.2.11
- optional: true
-
'@floating-ui/core@1.7.5':
dependencies:
'@floating-ui/utils': 0.2.11
- '@floating-ui/dom@1.7.4':
- dependencies:
- '@floating-ui/core': 1.7.3
- '@floating-ui/utils': 0.2.11
- optional: true
-
'@floating-ui/dom@1.7.6':
dependencies:
'@floating-ui/core': 1.7.5
@@ -10541,6 +10535,14 @@ snapshots:
'@formatjs/fast-memoize': 3.1.0
tslib: 2.8.1
+ '@gfazioli/mantine-onboarding-tour@4.0.2(@mantine/core@9.2.2(@mantine/hooks@9.2.2(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.2.2(react@19.2.6))(@tabler/icons-react@3.44.0(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':
+ dependencies:
+ '@mantine/core': 9.2.2(@mantine/hooks@9.2.2(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+ '@mantine/hooks': 9.2.2(react@19.2.6)
+ '@tabler/icons-react': 3.44.0(react@19.2.6)
+ react: 19.2.6
+ react-dom: 19.2.6(react@19.2.6)
+
'@gitbeaker/core@43.8.0':
dependencies:
'@gitbeaker/requester-utils': 43.8.0
@@ -10846,7 +10848,7 @@ snapshots:
'@types/node': 24.12.4
optional: true
- '@ioredis/commands@1.5.1': {}
+ '@ioredis/commands@1.10.0': {}
'@isaacs/cliui@8.0.2':
dependencies:
@@ -10939,22 +10941,22 @@ snapshots:
js-base64: 3.7.8
optional: true
- '@mantine/charts@9.2.1(@mantine/core@9.2.1(@mantine/hooks@9.2.1(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.2.1(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(recharts@3.8.1(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react-is@19.2.6)(react@19.2.6)(redux@5.0.1))':
+ '@mantine/charts@9.2.2(@mantine/core@9.2.2(@mantine/hooks@9.2.2(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.2.2(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(recharts@3.8.1(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react-is@19.2.6)(react@19.2.6)(redux@5.0.1))':
dependencies:
- '@mantine/core': 9.2.1(@mantine/hooks@9.2.1(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
- '@mantine/hooks': 9.2.1(react@19.2.6)
+ '@mantine/core': 9.2.2(@mantine/hooks@9.2.2(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+ '@mantine/hooks': 9.2.2(react@19.2.6)
react: 19.2.6
react-dom: 19.2.6(react@19.2.6)
recharts: 3.8.1(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react-is@19.2.6)(react@19.2.6)(redux@5.0.1)
- '@mantine/colors-generator@9.2.1(chroma-js@3.2.0)':
+ '@mantine/colors-generator@9.2.2(chroma-js@3.2.0)':
dependencies:
chroma-js: 3.2.0
- '@mantine/core@9.2.1(@mantine/hooks@9.2.1(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':
+ '@mantine/core@9.2.2(@mantine/hooks@9.2.2(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':
dependencies:
'@floating-ui/react': 0.27.19(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
- '@mantine/hooks': 9.2.1(react@19.2.6)
+ '@mantine/hooks': 9.2.2(react@19.2.6)
clsx: 2.1.1
react: 19.2.6
react-dom: 19.2.6(react@19.2.6)
@@ -10964,68 +10966,68 @@ snapshots:
transitivePeerDependencies:
- '@types/react'
- '@mantine/dates@9.2.1(@mantine/core@9.2.1(@mantine/hooks@9.2.1(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.2.1(react@19.2.6))(dayjs@1.11.20)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':
+ '@mantine/dates@9.2.2(@mantine/core@9.2.2(@mantine/hooks@9.2.2(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.2.2(react@19.2.6))(dayjs@1.11.21)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':
dependencies:
- '@mantine/core': 9.2.1(@mantine/hooks@9.2.1(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
- '@mantine/hooks': 9.2.1(react@19.2.6)
+ '@mantine/core': 9.2.2(@mantine/hooks@9.2.2(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+ '@mantine/hooks': 9.2.2(react@19.2.6)
clsx: 2.1.1
- dayjs: 1.11.20
+ dayjs: 1.11.21
react: 19.2.6
react-dom: 19.2.6(react@19.2.6)
- '@mantine/dropzone@9.2.1(@mantine/core@9.2.1(@mantine/hooks@9.2.1(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.2.1(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':
+ '@mantine/dropzone@9.2.2(@mantine/core@9.2.2(@mantine/hooks@9.2.2(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.2.2(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':
dependencies:
- '@mantine/core': 9.2.1(@mantine/hooks@9.2.1(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
- '@mantine/hooks': 9.2.1(react@19.2.6)
+ '@mantine/core': 9.2.2(@mantine/hooks@9.2.2(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+ '@mantine/hooks': 9.2.2(react@19.2.6)
react: 19.2.6
react-dom: 19.2.6(react@19.2.6)
react-dropzone: 15.0.0(react@19.2.6)
- '@mantine/form@9.2.1(react@19.2.6)':
+ '@mantine/form@9.2.2(react@19.2.6)':
dependencies:
'@standard-schema/spec': 1.1.0
fast-deep-equal: 3.1.3
klona: 2.0.6
react: 19.2.6
- '@mantine/hooks@9.2.1(react@19.2.6)':
+ '@mantine/hooks@9.2.2(react@19.2.6)':
dependencies:
react: 19.2.6
- '@mantine/modals@9.2.1(@mantine/core@9.2.1(@mantine/hooks@9.2.1(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.2.1(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':
+ '@mantine/modals@9.2.2(@mantine/core@9.2.2(@mantine/hooks@9.2.2(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.2.2(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':
dependencies:
- '@mantine/core': 9.2.1(@mantine/hooks@9.2.1(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
- '@mantine/hooks': 9.2.1(react@19.2.6)
+ '@mantine/core': 9.2.2(@mantine/hooks@9.2.2(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+ '@mantine/hooks': 9.2.2(react@19.2.6)
react: 19.2.6
react-dom: 19.2.6(react@19.2.6)
- '@mantine/notifications@9.2.1(@mantine/core@9.2.1(@mantine/hooks@9.2.1(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.2.1(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':
+ '@mantine/notifications@9.2.2(@mantine/core@9.2.2(@mantine/hooks@9.2.2(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.2.2(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':
dependencies:
- '@mantine/core': 9.2.1(@mantine/hooks@9.2.1(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
- '@mantine/hooks': 9.2.1(react@19.2.6)
- '@mantine/store': 9.2.1(react@19.2.6)
+ '@mantine/core': 9.2.2(@mantine/hooks@9.2.2(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+ '@mantine/hooks': 9.2.2(react@19.2.6)
+ '@mantine/store': 9.2.2(react@19.2.6)
react: 19.2.6
react-dom: 19.2.6(react@19.2.6)
react-transition-group: 4.4.5(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
- '@mantine/spotlight@9.2.1(@mantine/core@9.2.1(@mantine/hooks@9.2.1(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.2.1(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':
+ '@mantine/spotlight@9.2.2(@mantine/core@9.2.2(@mantine/hooks@9.2.2(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.2.2(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':
dependencies:
- '@mantine/core': 9.2.1(@mantine/hooks@9.2.1(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
- '@mantine/hooks': 9.2.1(react@19.2.6)
- '@mantine/store': 9.2.1(react@19.2.6)
+ '@mantine/core': 9.2.2(@mantine/hooks@9.2.2(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+ '@mantine/hooks': 9.2.2(react@19.2.6)
+ '@mantine/store': 9.2.2(react@19.2.6)
react: 19.2.6
react-dom: 19.2.6(react@19.2.6)
- '@mantine/store@9.2.1(react@19.2.6)':
+ '@mantine/store@9.2.2(react@19.2.6)':
dependencies:
react: 19.2.6
- '@mantine/tiptap@9.2.1(@mantine/core@9.2.1(@mantine/hooks@9.2.1(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.2.1(react@19.2.6))(@tiptap/extension-link@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6))(@tiptap/react@3.23.6(@floating-ui/dom@1.7.6)(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':
+ '@mantine/tiptap@9.2.2(@mantine/core@9.2.2(@mantine/hooks@9.2.2(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.2.2(react@19.2.6))(@tiptap/extension-link@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0))(@tiptap/react@3.24.0(@floating-ui/dom@1.7.6)(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':
dependencies:
- '@mantine/core': 9.2.1(@mantine/hooks@9.2.1(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
- '@mantine/hooks': 9.2.1(react@19.2.6)
- '@tiptap/extension-link': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)
- '@tiptap/react': 3.23.6(@floating-ui/dom@1.7.6)(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+ '@mantine/core': 9.2.2(@mantine/hooks@9.2.2(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+ '@mantine/hooks': 9.2.2(react@19.2.6)
+ '@tiptap/extension-link': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)
+ '@tiptap/react': 3.24.0(@floating-ui/dom@1.7.6)(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
react: 19.2.6
react-dom: 19.2.6(react@19.2.6)
@@ -11233,118 +11235,118 @@ snapshots:
'@open-draft/until@2.1.0':
optional: true
- '@oxfmt/binding-android-arm-eabi@0.51.0':
+ '@oxfmt/binding-android-arm-eabi@0.52.0':
optional: true
- '@oxfmt/binding-android-arm64@0.51.0':
+ '@oxfmt/binding-android-arm64@0.52.0':
optional: true
- '@oxfmt/binding-darwin-arm64@0.51.0':
+ '@oxfmt/binding-darwin-arm64@0.52.0':
optional: true
- '@oxfmt/binding-darwin-x64@0.51.0':
+ '@oxfmt/binding-darwin-x64@0.52.0':
optional: true
- '@oxfmt/binding-freebsd-x64@0.51.0':
+ '@oxfmt/binding-freebsd-x64@0.52.0':
optional: true
- '@oxfmt/binding-linux-arm-gnueabihf@0.51.0':
+ '@oxfmt/binding-linux-arm-gnueabihf@0.52.0':
optional: true
- '@oxfmt/binding-linux-arm-musleabihf@0.51.0':
+ '@oxfmt/binding-linux-arm-musleabihf@0.52.0':
optional: true
- '@oxfmt/binding-linux-arm64-gnu@0.51.0':
+ '@oxfmt/binding-linux-arm64-gnu@0.52.0':
optional: true
- '@oxfmt/binding-linux-arm64-musl@0.51.0':
+ '@oxfmt/binding-linux-arm64-musl@0.52.0':
optional: true
- '@oxfmt/binding-linux-ppc64-gnu@0.51.0':
+ '@oxfmt/binding-linux-ppc64-gnu@0.52.0':
optional: true
- '@oxfmt/binding-linux-riscv64-gnu@0.51.0':
+ '@oxfmt/binding-linux-riscv64-gnu@0.52.0':
optional: true
- '@oxfmt/binding-linux-riscv64-musl@0.51.0':
+ '@oxfmt/binding-linux-riscv64-musl@0.52.0':
optional: true
- '@oxfmt/binding-linux-s390x-gnu@0.51.0':
+ '@oxfmt/binding-linux-s390x-gnu@0.52.0':
optional: true
- '@oxfmt/binding-linux-x64-gnu@0.51.0':
+ '@oxfmt/binding-linux-x64-gnu@0.52.0':
optional: true
- '@oxfmt/binding-linux-x64-musl@0.51.0':
+ '@oxfmt/binding-linux-x64-musl@0.52.0':
optional: true
- '@oxfmt/binding-openharmony-arm64@0.51.0':
+ '@oxfmt/binding-openharmony-arm64@0.52.0':
optional: true
- '@oxfmt/binding-win32-arm64-msvc@0.51.0':
+ '@oxfmt/binding-win32-arm64-msvc@0.52.0':
optional: true
- '@oxfmt/binding-win32-ia32-msvc@0.51.0':
+ '@oxfmt/binding-win32-ia32-msvc@0.52.0':
optional: true
- '@oxfmt/binding-win32-x64-msvc@0.51.0':
+ '@oxfmt/binding-win32-x64-msvc@0.52.0':
optional: true
- '@oxlint/binding-android-arm-eabi@1.66.0':
+ '@oxlint/binding-android-arm-eabi@1.67.0':
optional: true
- '@oxlint/binding-android-arm64@1.66.0':
+ '@oxlint/binding-android-arm64@1.67.0':
optional: true
- '@oxlint/binding-darwin-arm64@1.66.0':
+ '@oxlint/binding-darwin-arm64@1.67.0':
optional: true
- '@oxlint/binding-darwin-x64@1.66.0':
+ '@oxlint/binding-darwin-x64@1.67.0':
optional: true
- '@oxlint/binding-freebsd-x64@1.66.0':
+ '@oxlint/binding-freebsd-x64@1.67.0':
optional: true
- '@oxlint/binding-linux-arm-gnueabihf@1.66.0':
+ '@oxlint/binding-linux-arm-gnueabihf@1.67.0':
optional: true
- '@oxlint/binding-linux-arm-musleabihf@1.66.0':
+ '@oxlint/binding-linux-arm-musleabihf@1.67.0':
optional: true
- '@oxlint/binding-linux-arm64-gnu@1.66.0':
+ '@oxlint/binding-linux-arm64-gnu@1.67.0':
optional: true
- '@oxlint/binding-linux-arm64-musl@1.66.0':
+ '@oxlint/binding-linux-arm64-musl@1.67.0':
optional: true
- '@oxlint/binding-linux-ppc64-gnu@1.66.0':
+ '@oxlint/binding-linux-ppc64-gnu@1.67.0':
optional: true
- '@oxlint/binding-linux-riscv64-gnu@1.66.0':
+ '@oxlint/binding-linux-riscv64-gnu@1.67.0':
optional: true
- '@oxlint/binding-linux-riscv64-musl@1.66.0':
+ '@oxlint/binding-linux-riscv64-musl@1.67.0':
optional: true
- '@oxlint/binding-linux-s390x-gnu@1.66.0':
+ '@oxlint/binding-linux-s390x-gnu@1.67.0':
optional: true
- '@oxlint/binding-linux-x64-gnu@1.66.0':
+ '@oxlint/binding-linux-x64-gnu@1.67.0':
optional: true
- '@oxlint/binding-linux-x64-musl@1.66.0':
+ '@oxlint/binding-linux-x64-musl@1.67.0':
optional: true
- '@oxlint/binding-openharmony-arm64@1.66.0':
+ '@oxlint/binding-openharmony-arm64@1.67.0':
optional: true
- '@oxlint/binding-win32-arm64-msvc@1.66.0':
+ '@oxlint/binding-win32-arm64-msvc@1.67.0':
optional: true
- '@oxlint/binding-win32-ia32-msvc@1.66.0':
+ '@oxlint/binding-win32-ia32-msvc@1.67.0':
optional: true
- '@oxlint/binding-win32-x64-msvc@1.66.0':
+ '@oxlint/binding-win32-x64-msvc@1.67.0':
optional: true
'@panva/hkdf@1.2.1': {}
@@ -12180,14 +12182,6 @@ snapshots:
'@tabler/icons@3.44.0': {}
- '@tanstack/devtools-event-client@0.4.0': {}
-
- '@tanstack/form-core@1.27.7':
- dependencies:
- '@tanstack/devtools-event-client': 0.4.0
- '@tanstack/pacer-lite': 0.1.1
- '@tanstack/store': 0.7.7
-
'@tanstack/match-sorter-utils@8.19.4':
dependencies:
remove-accents: 0.5.0
@@ -12225,203 +12219,200 @@ snapshots:
react: 19.2.6
react-dom: 19.2.6(react@19.2.6)
- '@tanstack/store@0.7.7': {}
-
- '@tanstack/store@0.8.0': {}
-
'@tanstack/table-core@8.20.5': {}
'@tanstack/virtual-core@3.11.2': {}
- '@testcontainers/mysql@12.0.0':
+ '@testcontainers/mysql@12.0.1':
dependencies:
- testcontainers: 12.0.0
+ testcontainers: 12.0.1
transitivePeerDependencies:
- bare-buffer
- supports-color
- '@testcontainers/postgresql@12.0.0':
+ '@testcontainers/postgresql@12.0.1':
dependencies:
- testcontainers: 12.0.0
+ testcontainers: 12.0.1
transitivePeerDependencies:
- bare-buffer
- supports-color
- '@testcontainers/redis@12.0.0':
+ '@testcontainers/redis@12.0.1':
dependencies:
- testcontainers: 12.0.0
+ testcontainers: 12.0.1
transitivePeerDependencies:
- bare-buffer
- supports-color
- '@tiptap/core@3.23.6(@tiptap/pm@3.23.6)':
+ '@tiptap/core@3.24.0(@tiptap/pm@3.24.0)':
dependencies:
- '@tiptap/pm': 3.23.6
+ '@tiptap/pm': 3.24.0
- '@tiptap/extension-blockquote@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))':
+ '@tiptap/extension-blockquote@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))':
dependencies:
- '@tiptap/core': 3.23.6(@tiptap/pm@3.23.6)
+ '@tiptap/core': 3.24.0(@tiptap/pm@3.24.0)
- '@tiptap/extension-bold@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))':
+ '@tiptap/extension-bold@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))':
dependencies:
- '@tiptap/core': 3.23.6(@tiptap/pm@3.23.6)
+ '@tiptap/core': 3.24.0(@tiptap/pm@3.24.0)
- '@tiptap/extension-bubble-menu@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)':
- dependencies:
- '@floating-ui/dom': 1.7.4
- '@tiptap/core': 3.23.6(@tiptap/pm@3.23.6)
- '@tiptap/pm': 3.23.6
- optional: true
-
- '@tiptap/extension-bullet-list@3.23.6(@tiptap/extension-list@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6))':
- dependencies:
- '@tiptap/extension-list': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)
-
- '@tiptap/extension-code-block@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)':
- dependencies:
- '@tiptap/core': 3.23.6(@tiptap/pm@3.23.6)
- '@tiptap/pm': 3.23.6
-
- '@tiptap/extension-code@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))':
- dependencies:
- '@tiptap/core': 3.23.6(@tiptap/pm@3.23.6)
-
- '@tiptap/extension-color@3.23.6(@tiptap/extension-text-style@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6)))':
- dependencies:
- '@tiptap/extension-text-style': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))
-
- '@tiptap/extension-document@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))':
- dependencies:
- '@tiptap/core': 3.23.6(@tiptap/pm@3.23.6)
-
- '@tiptap/extension-dropcursor@3.23.6(@tiptap/extensions@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6))':
- dependencies:
- '@tiptap/extensions': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)
-
- '@tiptap/extension-floating-menu@3.23.6(@floating-ui/dom@1.7.6)(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)':
+ '@tiptap/extension-bubble-menu@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)':
dependencies:
'@floating-ui/dom': 1.7.6
- '@tiptap/core': 3.23.6(@tiptap/pm@3.23.6)
- '@tiptap/pm': 3.23.6
+ '@tiptap/core': 3.24.0(@tiptap/pm@3.24.0)
+ '@tiptap/pm': 3.24.0
optional: true
- '@tiptap/extension-gapcursor@3.23.6(@tiptap/extensions@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6))':
+ '@tiptap/extension-bullet-list@3.24.0(@tiptap/extension-list@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0))':
dependencies:
- '@tiptap/extensions': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)
+ '@tiptap/extension-list': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)
- '@tiptap/extension-hard-break@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))':
+ '@tiptap/extension-code-block@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)':
dependencies:
- '@tiptap/core': 3.23.6(@tiptap/pm@3.23.6)
+ '@tiptap/core': 3.24.0(@tiptap/pm@3.24.0)
+ '@tiptap/pm': 3.24.0
- '@tiptap/extension-heading@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))':
+ '@tiptap/extension-code@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))':
dependencies:
- '@tiptap/core': 3.23.6(@tiptap/pm@3.23.6)
+ '@tiptap/core': 3.24.0(@tiptap/pm@3.24.0)
- '@tiptap/extension-highlight@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))':
+ '@tiptap/extension-color@3.24.0(@tiptap/extension-text-style@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0)))':
dependencies:
- '@tiptap/core': 3.23.6(@tiptap/pm@3.23.6)
+ '@tiptap/extension-text-style': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))
- '@tiptap/extension-horizontal-rule@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)':
+ '@tiptap/extension-document@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))':
dependencies:
- '@tiptap/core': 3.23.6(@tiptap/pm@3.23.6)
- '@tiptap/pm': 3.23.6
+ '@tiptap/core': 3.24.0(@tiptap/pm@3.24.0)
- '@tiptap/extension-image@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))':
+ '@tiptap/extension-dropcursor@3.24.0(@tiptap/extensions@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0))':
dependencies:
- '@tiptap/core': 3.23.6(@tiptap/pm@3.23.6)
+ '@tiptap/extensions': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)
- '@tiptap/extension-italic@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))':
+ '@tiptap/extension-floating-menu@3.24.0(@floating-ui/dom@1.7.6)(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)':
dependencies:
- '@tiptap/core': 3.23.6(@tiptap/pm@3.23.6)
+ '@floating-ui/dom': 1.7.6
+ '@tiptap/core': 3.24.0(@tiptap/pm@3.24.0)
+ '@tiptap/pm': 3.24.0
+ optional: true
- '@tiptap/extension-link@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)':
+ '@tiptap/extension-gapcursor@3.24.0(@tiptap/extensions@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0))':
dependencies:
- '@tiptap/core': 3.23.6(@tiptap/pm@3.23.6)
- '@tiptap/pm': 3.23.6
+ '@tiptap/extensions': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)
+
+ '@tiptap/extension-hard-break@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))':
+ dependencies:
+ '@tiptap/core': 3.24.0(@tiptap/pm@3.24.0)
+
+ '@tiptap/extension-heading@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))':
+ dependencies:
+ '@tiptap/core': 3.24.0(@tiptap/pm@3.24.0)
+
+ '@tiptap/extension-highlight@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))':
+ dependencies:
+ '@tiptap/core': 3.24.0(@tiptap/pm@3.24.0)
+
+ '@tiptap/extension-horizontal-rule@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)':
+ dependencies:
+ '@tiptap/core': 3.24.0(@tiptap/pm@3.24.0)
+ '@tiptap/pm': 3.24.0
+
+ '@tiptap/extension-image@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))':
+ dependencies:
+ '@tiptap/core': 3.24.0(@tiptap/pm@3.24.0)
+
+ '@tiptap/extension-italic@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))':
+ dependencies:
+ '@tiptap/core': 3.24.0(@tiptap/pm@3.24.0)
+
+ '@tiptap/extension-link@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)':
+ dependencies:
+ '@tiptap/core': 3.24.0(@tiptap/pm@3.24.0)
+ '@tiptap/pm': 3.24.0
linkifyjs: 4.3.3
- '@tiptap/extension-list-item@3.23.6(@tiptap/extension-list@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6))':
+ '@tiptap/extension-list-item@3.24.0(@tiptap/extension-list@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0))':
dependencies:
- '@tiptap/extension-list': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)
+ '@tiptap/extension-list': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)
- '@tiptap/extension-list-keymap@3.23.6(@tiptap/extension-list@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6))':
+ '@tiptap/extension-list-keymap@3.24.0(@tiptap/extension-list@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0))':
dependencies:
- '@tiptap/extension-list': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)
+ '@tiptap/extension-list': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)
- '@tiptap/extension-list@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)':
+ '@tiptap/extension-list@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)':
dependencies:
- '@tiptap/core': 3.23.6(@tiptap/pm@3.23.6)
- '@tiptap/pm': 3.23.6
+ '@tiptap/core': 3.24.0(@tiptap/pm@3.24.0)
+ '@tiptap/pm': 3.24.0
- '@tiptap/extension-ordered-list@3.23.6(@tiptap/extension-list@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6))':
+ '@tiptap/extension-ordered-list@3.24.0(@tiptap/extension-list@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0))':
dependencies:
- '@tiptap/extension-list': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)
+ '@tiptap/extension-list': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)
- '@tiptap/extension-paragraph@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))':
+ '@tiptap/extension-paragraph@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))':
dependencies:
- '@tiptap/core': 3.23.6(@tiptap/pm@3.23.6)
+ '@tiptap/core': 3.24.0(@tiptap/pm@3.24.0)
- '@tiptap/extension-placeholder@3.23.6(@tiptap/extensions@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6))':
+ '@tiptap/extension-placeholder@3.24.0(@tiptap/extensions@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0))':
dependencies:
- '@tiptap/extensions': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)
+ '@tiptap/extensions': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)
- '@tiptap/extension-strike@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))':
+ '@tiptap/extension-strike@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))':
dependencies:
- '@tiptap/core': 3.23.6(@tiptap/pm@3.23.6)
+ '@tiptap/core': 3.24.0(@tiptap/pm@3.24.0)
- '@tiptap/extension-table-cell@3.23.6(@tiptap/extension-table@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6))':
+ '@tiptap/extension-table-cell@3.24.0(@tiptap/extension-table@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0))':
dependencies:
- '@tiptap/extension-table': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)
+ '@tiptap/extension-table': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)
- '@tiptap/extension-table-header@3.23.6(@tiptap/extension-table@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6))':
+ '@tiptap/extension-table-header@3.24.0(@tiptap/extension-table@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0))':
dependencies:
- '@tiptap/extension-table': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)
+ '@tiptap/extension-table': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)
- '@tiptap/extension-table-row@3.23.6(@tiptap/extension-table@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6))':
+ '@tiptap/extension-table-row@3.24.0(@tiptap/extension-table@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0))':
dependencies:
- '@tiptap/extension-table': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)
+ '@tiptap/extension-table': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)
- '@tiptap/extension-table@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)':
+ '@tiptap/extension-table@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)':
dependencies:
- '@tiptap/core': 3.23.6(@tiptap/pm@3.23.6)
- '@tiptap/pm': 3.23.6
+ '@tiptap/core': 3.24.0(@tiptap/pm@3.24.0)
+ '@tiptap/pm': 3.24.0
- '@tiptap/extension-task-item@3.23.6(@tiptap/extension-list@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6))':
+ '@tiptap/extension-task-item@3.24.0(@tiptap/extension-list@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0))':
dependencies:
- '@tiptap/extension-list': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)
+ '@tiptap/extension-list': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)
- '@tiptap/extension-task-list@3.23.6(@tiptap/extension-list@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6))':
+ '@tiptap/extension-task-list@3.24.0(@tiptap/extension-list@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0))':
dependencies:
- '@tiptap/extension-list': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)
+ '@tiptap/extension-list': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)
- '@tiptap/extension-text-align@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))':
+ '@tiptap/extension-text-align@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))':
dependencies:
- '@tiptap/core': 3.23.6(@tiptap/pm@3.23.6)
+ '@tiptap/core': 3.24.0(@tiptap/pm@3.24.0)
- '@tiptap/extension-text-style@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))':
+ '@tiptap/extension-text-style@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))':
dependencies:
- '@tiptap/core': 3.23.6(@tiptap/pm@3.23.6)
+ '@tiptap/core': 3.24.0(@tiptap/pm@3.24.0)
- '@tiptap/extension-text@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))':
+ '@tiptap/extension-text@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))':
dependencies:
- '@tiptap/core': 3.23.6(@tiptap/pm@3.23.6)
+ '@tiptap/core': 3.24.0(@tiptap/pm@3.24.0)
- '@tiptap/extension-underline@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))':
+ '@tiptap/extension-underline@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))':
dependencies:
- '@tiptap/core': 3.23.6(@tiptap/pm@3.23.6)
+ '@tiptap/core': 3.24.0(@tiptap/pm@3.24.0)
- '@tiptap/extensions@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)':
+ '@tiptap/extensions@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)':
dependencies:
- '@tiptap/core': 3.23.6(@tiptap/pm@3.23.6)
- '@tiptap/pm': 3.23.6
+ '@tiptap/core': 3.24.0(@tiptap/pm@3.24.0)
+ '@tiptap/pm': 3.24.0
- '@tiptap/pm@3.23.6':
+ '@tiptap/pm@3.24.0':
dependencies:
prosemirror-changeset: 2.3.0
prosemirror-commands: 1.6.2
prosemirror-dropcursor: 1.8.1
prosemirror-gapcursor: 1.3.2
prosemirror-history: 1.4.1
+ prosemirror-inputrules: 1.5.1
prosemirror-keymap: 1.2.2
prosemirror-model: 1.24.1
prosemirror-schema-list: 1.5.1
@@ -12430,10 +12421,10 @@ snapshots:
prosemirror-transform: 1.10.2
prosemirror-view: 1.41.4
- '@tiptap/react@3.23.6(@floating-ui/dom@1.7.6)(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':
+ '@tiptap/react@3.24.0(@floating-ui/dom@1.7.6)(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':
dependencies:
- '@tiptap/core': 3.23.6(@tiptap/pm@3.23.6)
- '@tiptap/pm': 3.23.6
+ '@tiptap/core': 3.24.0(@tiptap/pm@3.24.0)
+ '@tiptap/pm': 3.24.0
'@types/react': 19.2.15
'@types/react-dom': 19.2.3(@types/react@19.2.15)
'@types/use-sync-external-store': 0.0.6
@@ -12442,37 +12433,37 @@ snapshots:
react-dom: 19.2.6(react@19.2.6)
use-sync-external-store: 1.6.0(react@19.2.6)
optionalDependencies:
- '@tiptap/extension-bubble-menu': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)
- '@tiptap/extension-floating-menu': 3.23.6(@floating-ui/dom@1.7.6)(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)
+ '@tiptap/extension-bubble-menu': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)
+ '@tiptap/extension-floating-menu': 3.24.0(@floating-ui/dom@1.7.6)(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)
transitivePeerDependencies:
- '@floating-ui/dom'
- '@tiptap/starter-kit@3.23.6':
+ '@tiptap/starter-kit@3.24.0':
dependencies:
- '@tiptap/core': 3.23.6(@tiptap/pm@3.23.6)
- '@tiptap/extension-blockquote': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))
- '@tiptap/extension-bold': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))
- '@tiptap/extension-bullet-list': 3.23.6(@tiptap/extension-list@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6))
- '@tiptap/extension-code': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))
- '@tiptap/extension-code-block': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)
- '@tiptap/extension-document': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))
- '@tiptap/extension-dropcursor': 3.23.6(@tiptap/extensions@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6))
- '@tiptap/extension-gapcursor': 3.23.6(@tiptap/extensions@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6))
- '@tiptap/extension-hard-break': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))
- '@tiptap/extension-heading': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))
- '@tiptap/extension-horizontal-rule': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)
- '@tiptap/extension-italic': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))
- '@tiptap/extension-link': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)
- '@tiptap/extension-list': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)
- '@tiptap/extension-list-item': 3.23.6(@tiptap/extension-list@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6))
- '@tiptap/extension-list-keymap': 3.23.6(@tiptap/extension-list@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6))
- '@tiptap/extension-ordered-list': 3.23.6(@tiptap/extension-list@3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6))
- '@tiptap/extension-paragraph': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))
- '@tiptap/extension-strike': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))
- '@tiptap/extension-text': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))
- '@tiptap/extension-underline': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))
- '@tiptap/extensions': 3.23.6(@tiptap/core@3.23.6(@tiptap/pm@3.23.6))(@tiptap/pm@3.23.6)
- '@tiptap/pm': 3.23.6
+ '@tiptap/core': 3.24.0(@tiptap/pm@3.24.0)
+ '@tiptap/extension-blockquote': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))
+ '@tiptap/extension-bold': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))
+ '@tiptap/extension-bullet-list': 3.24.0(@tiptap/extension-list@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0))
+ '@tiptap/extension-code': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))
+ '@tiptap/extension-code-block': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)
+ '@tiptap/extension-document': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))
+ '@tiptap/extension-dropcursor': 3.24.0(@tiptap/extensions@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0))
+ '@tiptap/extension-gapcursor': 3.24.0(@tiptap/extensions@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0))
+ '@tiptap/extension-hard-break': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))
+ '@tiptap/extension-heading': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))
+ '@tiptap/extension-horizontal-rule': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)
+ '@tiptap/extension-italic': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))
+ '@tiptap/extension-link': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)
+ '@tiptap/extension-list': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)
+ '@tiptap/extension-list-item': 3.24.0(@tiptap/extension-list@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0))
+ '@tiptap/extension-list-keymap': 3.24.0(@tiptap/extension-list@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0))
+ '@tiptap/extension-ordered-list': 3.24.0(@tiptap/extension-list@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0))
+ '@tiptap/extension-paragraph': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))
+ '@tiptap/extension-strike': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))
+ '@tiptap/extension-text': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))
+ '@tiptap/extension-underline': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))
+ '@tiptap/extensions': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)
+ '@tiptap/pm': 3.24.0
'@tree-sitter-grammars/tree-sitter-yaml@0.7.1(tree-sitter@0.22.4)':
dependencies:
@@ -12519,29 +12510,29 @@ snapshots:
react: 19.2.6
typescript: 5.9.3
- '@turbo/darwin-64@2.9.14':
+ '@turbo/darwin-64@2.9.16':
optional: true
- '@turbo/darwin-arm64@2.9.14':
+ '@turbo/darwin-arm64@2.9.16':
optional: true
- '@turbo/gen@2.9.14(@types/node@24.12.4)':
+ '@turbo/gen@2.9.16(@types/node@24.12.4)':
dependencies:
'@inquirer/prompts': 7.10.1(@types/node@24.12.4)
esbuild: 0.25.12
transitivePeerDependencies:
- '@types/node'
- '@turbo/linux-64@2.9.14':
+ '@turbo/linux-64@2.9.16':
optional: true
- '@turbo/linux-arm64@2.9.14':
+ '@turbo/linux-arm64@2.9.16':
optional: true
- '@turbo/windows-64@2.9.14':
+ '@turbo/windows-64@2.9.16':
optional: true
- '@turbo/windows-arm64@2.9.14':
+ '@turbo/windows-arm64@2.9.16':
optional: true
'@types/adm-zip@0.5.8':
@@ -12798,7 +12789,7 @@ snapshots:
'@videojs/http-streaming@3.17.4(video.js@8.23.7)':
dependencies:
- '@babel/runtime': 7.29.2
+ '@babel/runtime': 7.29.7
'@videojs/vhs-utils': 4.1.1
aes-decrypter: 4.0.2
global: 4.4.0
@@ -12809,18 +12800,18 @@ snapshots:
'@videojs/vhs-utils@4.0.0':
dependencies:
- '@babel/runtime': 7.29.2
+ '@babel/runtime': 7.29.7
global: 4.4.0
url-toolkit: 2.2.5
'@videojs/vhs-utils@4.1.1':
dependencies:
- '@babel/runtime': 7.29.2
+ '@babel/runtime': 7.29.7
global: 4.4.0
'@videojs/xhr@2.7.0':
dependencies:
- '@babel/runtime': 7.29.2
+ '@babel/runtime': 7.29.7
global: 4.4.0
is-function: 1.0.2
@@ -13009,7 +13000,7 @@ snapshots:
aes-decrypter@4.0.2:
dependencies:
- '@babel/runtime': 7.29.2
+ '@babel/runtime': 7.29.7
'@videojs/vhs-utils': 4.1.1
global: 4.4.0
pkcs7: 1.0.4
@@ -13388,7 +13379,7 @@ snapshots:
clsx@2.1.1: {}
- cluster-key-slot@1.1.2: {}
+ cluster-key-slot@1.1.1: {}
co-body@6.2.0:
dependencies:
@@ -13610,7 +13601,7 @@ snapshots:
transitivePeerDependencies:
- '@noble/hashes'
- dayjs@1.11.20: {}
+ dayjs@1.11.21: {}
debug@4.4.3:
dependencies:
@@ -13699,7 +13690,7 @@ snapshots:
dom-helpers@5.2.1:
dependencies:
- '@babel/runtime': 7.29.2
+ '@babel/runtime': 7.29.7
csstype: 3.2.3
dom-walk@0.1.2: {}
@@ -13708,6 +13699,10 @@ snapshots:
optionalDependencies:
'@types/trusted-types': 2.0.7
+ dompurify@3.4.7:
+ optionalDependencies:
+ '@types/trusted-types': 2.0.7
+
dot-prop@5.3.0:
dependencies:
is-obj: 2.0.0
@@ -13736,19 +13731,19 @@ snapshots:
esbuild: 0.25.12
tsx: 4.21.0
- drizzle-orm@0.45.2(@libsql/client-wasm@0.14.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.10.0)(gel@2.0.0)(mysql2@3.22.3(@types/node@24.12.4))(pg@8.21.0):
+ drizzle-orm@0.45.2(@libsql/client-wasm@0.14.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.10.0)(gel@2.0.0)(mysql2@3.22.4(@types/node@24.12.4))(pg@8.21.0):
optionalDependencies:
'@libsql/client-wasm': 0.14.0
'@types/better-sqlite3': 7.6.13
'@types/pg': 8.20.0
better-sqlite3: 12.10.0
gel: 2.0.0
- mysql2: 3.22.3(@types/node@24.12.4)
+ mysql2: 3.22.4(@types/node@24.12.4)
pg: 8.21.0
- drizzle-zod@0.8.3(drizzle-orm@0.45.2(@libsql/client-wasm@0.14.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.10.0)(gel@2.0.0)(mysql2@3.22.3(@types/node@24.12.4))(pg@8.21.0))(zod@4.4.3):
+ drizzle-zod@0.8.3(drizzle-orm@0.45.2(@libsql/client-wasm@0.14.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.10.0)(gel@2.0.0)(mysql2@3.22.4(@types/node@24.12.4))(pg@8.21.0))(zod@4.4.3):
dependencies:
- drizzle-orm: 0.45.2(@libsql/client-wasm@0.14.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.10.0)(gel@2.0.0)(mysql2@3.22.3(@types/node@24.12.4))(pg@8.21.0)
+ drizzle-orm: 0.45.2(@libsql/client-wasm@0.14.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.10.0)(gel@2.0.0)(mysql2@3.22.4(@types/node@24.12.4))(pg@8.21.0)
zod: 4.4.3
dunder-proto@1.0.1:
@@ -14418,7 +14413,7 @@ snapshots:
dependencies:
safer-buffer: 2.1.2
- icu-minify@4.12.0:
+ icu-minify@4.13.0:
dependencies:
'@formatjs/icu-messageformat-parser': 3.5.0
@@ -14473,14 +14468,12 @@ snapshots:
dependencies:
loose-envify: 1.4.0
- ioredis@5.10.1:
+ ioredis@5.11.0:
dependencies:
- '@ioredis/commands': 1.5.1
- cluster-key-slot: 1.1.2
+ '@ioredis/commands': 1.10.0
+ cluster-key-slot: 1.1.1
debug: 4.4.3
denque: 2.1.0
- lodash.defaults: 4.2.0
- lodash.isarguments: 3.1.0
redis-errors: 1.2.0
redis-parser: 3.0.0
standard-as-callback: 2.1.0
@@ -14559,9 +14552,9 @@ snapshots:
isexe@3.1.5:
optional: true
- isomorphic-dompurify@3.14.0(@noble/hashes@2.0.1):
+ isomorphic-dompurify@3.15.0(@noble/hashes@2.0.1):
dependencies:
- dompurify: 3.4.5
+ dompurify: 3.4.7
jsdom: 29.1.1(@noble/hashes@2.0.1)
transitivePeerDependencies:
- '@noble/hashes'
@@ -14751,12 +14744,8 @@ snapshots:
lodash.debounce@4.0.8: {}
- lodash.defaults@4.2.0: {}
-
lodash.escaperegexp@4.1.2: {}
- lodash.isarguments@3.1.0: {}
-
lodash.isequal@4.5.0: {}
lodash.isplainobject@4.0.6: {}
@@ -14801,7 +14790,7 @@ snapshots:
m3u8-parser@7.2.0:
dependencies:
- '@babel/runtime': 7.29.2
+ '@babel/runtime': 7.29.7
'@videojs/vhs-utils': 4.1.1
global: 4.4.0
@@ -14819,17 +14808,17 @@ snapshots:
dependencies:
semver: 7.7.4
- mantine-react-table@2.0.0-beta.9(@mantine/core@9.2.1(@mantine/hooks@9.2.1(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/dates@9.2.1(@mantine/core@9.2.1(@mantine/hooks@9.2.1(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.2.1(react@19.2.6))(dayjs@1.11.20)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.2.1(react@19.2.6))(@tabler/icons-react@3.44.0(react@19.2.6))(clsx@2.1.1)(dayjs@1.11.20)(react-dom@19.2.6(react@19.2.6))(react@19.2.6):
+ mantine-react-table-open@9.0.3(@mantine/core@9.2.2(@mantine/hooks@9.2.2(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/dates@9.2.2(@mantine/core@9.2.2(@mantine/hooks@9.2.2(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.2.2(react@19.2.6))(dayjs@1.11.21)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.2.2(react@19.2.6))(@tabler/icons-react@3.44.0(react@19.2.6))(clsx@2.1.1)(dayjs@1.11.21)(react-dom@19.2.6(react@19.2.6))(react@19.2.6):
dependencies:
- '@mantine/core': 9.2.1(@mantine/hooks@9.2.1(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
- '@mantine/dates': 9.2.1(@mantine/core@9.2.1(@mantine/hooks@9.2.1(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.2.1(react@19.2.6))(dayjs@1.11.20)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
- '@mantine/hooks': 9.2.1(react@19.2.6)
+ '@mantine/core': 9.2.2(@mantine/hooks@9.2.2(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+ '@mantine/dates': 9.2.2(@mantine/core@9.2.2(@mantine/hooks@9.2.2(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.2.2(react@19.2.6))(dayjs@1.11.21)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+ '@mantine/hooks': 9.2.2(react@19.2.6)
'@tabler/icons-react': 3.44.0(react@19.2.6)
'@tanstack/match-sorter-utils': 8.19.4
'@tanstack/react-table': 8.20.5(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@tanstack/react-virtual': 3.11.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
clsx: 2.1.1
- dayjs: 1.11.20
+ dayjs: 1.11.21
react: 19.2.6
react-dom: 19.2.6(react@19.2.6)
@@ -15131,7 +15120,7 @@ snapshots:
mpd-parser@1.3.1:
dependencies:
- '@babel/runtime': 7.29.2
+ '@babel/runtime': 7.29.7
'@videojs/vhs-utils': 4.0.0
'@xmldom/xmldom': 0.8.10
global: 4.4.0
@@ -15173,10 +15162,10 @@ snapshots:
mux.js@7.1.0:
dependencies:
- '@babel/runtime': 7.29.2
+ '@babel/runtime': 7.29.7
global: 4.4.0
- mysql2@3.22.3(@types/node@24.12.4):
+ mysql2@3.22.4(@types/node@24.12.4):
dependencies:
'@types/node': 24.12.4
aws-ssl-profiles: 1.1.2
@@ -15219,20 +15208,20 @@ snapshots:
next: 16.2.6(@babel/core@7.29.0)(@playwright/test@1.60.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.100.0)
react: 19.2.6
- next-intl-swc-plugin-extractor@4.12.0: {}
+ next-intl-swc-plugin-extractor@4.13.0: {}
- next-intl@4.12.0(next@16.2.6(@babel/core@7.29.0)(@playwright/test@1.60.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.100.0))(react@19.2.6)(typescript@5.9.3):
+ next-intl@4.13.0(next@16.2.6(@babel/core@7.29.0)(@playwright/test@1.60.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.100.0))(react@19.2.6)(typescript@5.9.3):
dependencies:
'@formatjs/intl-localematcher': 0.8.1
'@parcel/watcher': 2.4.1
'@swc/core': 1.15.3
- icu-minify: 4.12.0
+ icu-minify: 4.13.0
negotiator: 1.0.0
next: 16.2.6(@babel/core@7.29.0)(@playwright/test@1.60.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.100.0)
- next-intl-swc-plugin-extractor: 4.12.0
+ next-intl-swc-plugin-extractor: 4.13.0
po-parser: 2.1.1
react: 19.2.6
- use-intl: 4.12.0(react@19.2.6)
+ use-intl: 4.13.0(react@19.2.6)
optionalDependencies:
typescript: 5.9.3
transitivePeerDependencies:
@@ -15398,51 +15387,51 @@ snapshots:
outvariant@1.4.3:
optional: true
- oxfmt@0.51.0:
+ oxfmt@0.52.0:
dependencies:
tinypool: 2.1.0
optionalDependencies:
- '@oxfmt/binding-android-arm-eabi': 0.51.0
- '@oxfmt/binding-android-arm64': 0.51.0
- '@oxfmt/binding-darwin-arm64': 0.51.0
- '@oxfmt/binding-darwin-x64': 0.51.0
- '@oxfmt/binding-freebsd-x64': 0.51.0
- '@oxfmt/binding-linux-arm-gnueabihf': 0.51.0
- '@oxfmt/binding-linux-arm-musleabihf': 0.51.0
- '@oxfmt/binding-linux-arm64-gnu': 0.51.0
- '@oxfmt/binding-linux-arm64-musl': 0.51.0
- '@oxfmt/binding-linux-ppc64-gnu': 0.51.0
- '@oxfmt/binding-linux-riscv64-gnu': 0.51.0
- '@oxfmt/binding-linux-riscv64-musl': 0.51.0
- '@oxfmt/binding-linux-s390x-gnu': 0.51.0
- '@oxfmt/binding-linux-x64-gnu': 0.51.0
- '@oxfmt/binding-linux-x64-musl': 0.51.0
- '@oxfmt/binding-openharmony-arm64': 0.51.0
- '@oxfmt/binding-win32-arm64-msvc': 0.51.0
- '@oxfmt/binding-win32-ia32-msvc': 0.51.0
- '@oxfmt/binding-win32-x64-msvc': 0.51.0
+ '@oxfmt/binding-android-arm-eabi': 0.52.0
+ '@oxfmt/binding-android-arm64': 0.52.0
+ '@oxfmt/binding-darwin-arm64': 0.52.0
+ '@oxfmt/binding-darwin-x64': 0.52.0
+ '@oxfmt/binding-freebsd-x64': 0.52.0
+ '@oxfmt/binding-linux-arm-gnueabihf': 0.52.0
+ '@oxfmt/binding-linux-arm-musleabihf': 0.52.0
+ '@oxfmt/binding-linux-arm64-gnu': 0.52.0
+ '@oxfmt/binding-linux-arm64-musl': 0.52.0
+ '@oxfmt/binding-linux-ppc64-gnu': 0.52.0
+ '@oxfmt/binding-linux-riscv64-gnu': 0.52.0
+ '@oxfmt/binding-linux-riscv64-musl': 0.52.0
+ '@oxfmt/binding-linux-s390x-gnu': 0.52.0
+ '@oxfmt/binding-linux-x64-gnu': 0.52.0
+ '@oxfmt/binding-linux-x64-musl': 0.52.0
+ '@oxfmt/binding-openharmony-arm64': 0.52.0
+ '@oxfmt/binding-win32-arm64-msvc': 0.52.0
+ '@oxfmt/binding-win32-ia32-msvc': 0.52.0
+ '@oxfmt/binding-win32-x64-msvc': 0.52.0
- oxlint@1.66.0:
+ oxlint@1.67.0:
optionalDependencies:
- '@oxlint/binding-android-arm-eabi': 1.66.0
- '@oxlint/binding-android-arm64': 1.66.0
- '@oxlint/binding-darwin-arm64': 1.66.0
- '@oxlint/binding-darwin-x64': 1.66.0
- '@oxlint/binding-freebsd-x64': 1.66.0
- '@oxlint/binding-linux-arm-gnueabihf': 1.66.0
- '@oxlint/binding-linux-arm-musleabihf': 1.66.0
- '@oxlint/binding-linux-arm64-gnu': 1.66.0
- '@oxlint/binding-linux-arm64-musl': 1.66.0
- '@oxlint/binding-linux-ppc64-gnu': 1.66.0
- '@oxlint/binding-linux-riscv64-gnu': 1.66.0
- '@oxlint/binding-linux-riscv64-musl': 1.66.0
- '@oxlint/binding-linux-s390x-gnu': 1.66.0
- '@oxlint/binding-linux-x64-gnu': 1.66.0
- '@oxlint/binding-linux-x64-musl': 1.66.0
- '@oxlint/binding-openharmony-arm64': 1.66.0
- '@oxlint/binding-win32-arm64-msvc': 1.66.0
- '@oxlint/binding-win32-ia32-msvc': 1.66.0
- '@oxlint/binding-win32-x64-msvc': 1.66.0
+ '@oxlint/binding-android-arm-eabi': 1.67.0
+ '@oxlint/binding-android-arm64': 1.67.0
+ '@oxlint/binding-darwin-arm64': 1.67.0
+ '@oxlint/binding-darwin-x64': 1.67.0
+ '@oxlint/binding-freebsd-x64': 1.67.0
+ '@oxlint/binding-linux-arm-gnueabihf': 1.67.0
+ '@oxlint/binding-linux-arm-musleabihf': 1.67.0
+ '@oxlint/binding-linux-arm64-gnu': 1.67.0
+ '@oxlint/binding-linux-arm64-musl': 1.67.0
+ '@oxlint/binding-linux-ppc64-gnu': 1.67.0
+ '@oxlint/binding-linux-riscv64-gnu': 1.67.0
+ '@oxlint/binding-linux-riscv64-musl': 1.67.0
+ '@oxlint/binding-linux-s390x-gnu': 1.67.0
+ '@oxlint/binding-linux-x64-gnu': 1.67.0
+ '@oxlint/binding-linux-x64-musl': 1.67.0
+ '@oxlint/binding-openharmony-arm64': 1.67.0
+ '@oxlint/binding-win32-arm64-msvc': 1.67.0
+ '@oxlint/binding-win32-ia32-msvc': 1.67.0
+ '@oxlint/binding-win32-x64-msvc': 1.67.0
p-each-series@3.0.0: {}
@@ -15610,7 +15599,7 @@ snapshots:
pkcs7@1.0.4:
dependencies:
- '@babel/runtime': 7.29.2
+ '@babel/runtime': 7.29.7
pkg-conf@2.1.0:
dependencies:
@@ -15772,6 +15761,11 @@ snapshots:
prosemirror-view: 1.41.4
rope-sequence: 1.3.4
+ prosemirror-inputrules@1.5.1:
+ dependencies:
+ prosemirror-state: 1.4.3
+ prosemirror-transform: 1.10.2
+
prosemirror-keymap@1.2.2:
dependencies:
prosemirror-state: 1.4.3
@@ -15921,7 +15915,7 @@ snapshots:
prop-types: 15.8.1
react: 19.2.6
- react-error-boundary@6.1.1(react@19.2.6):
+ react-error-boundary@6.1.2(react@19.2.6):
dependencies:
react: 19.2.6
@@ -16028,7 +16022,7 @@ snapshots:
react-transition-group@4.4.5(react-dom@19.2.6(react@19.2.6))(react@19.2.6):
dependencies:
- '@babel/runtime': 7.29.2
+ '@babel/runtime': 7.29.7
dom-helpers: 5.2.1
loose-envify: 1.4.0
prop-types: 15.8.1
@@ -16779,7 +16773,7 @@ snapshots:
commander: 2.20.3
source-map-support: 0.5.21
- testcontainers@12.0.0:
+ testcontainers@12.0.1:
dependencies:
'@balena/dockerignore': 1.0.2
'@types/dockerode': 4.0.1
@@ -16794,7 +16788,7 @@ snapshots:
properties-reader: 3.0.1
ssh-remote-port-forward: 1.0.4
tar-fs: 3.1.2
- tmp: 0.2.5
+ tmp: 0.2.7
undici: 7.25.0
transitivePeerDependencies:
- bare-buffer
@@ -16856,7 +16850,7 @@ snapshots:
dependencies:
tldts-core: 7.0.12
- tmp@0.2.5: {}
+ tmp@0.2.7: {}
to-buffer@1.2.1:
dependencies:
@@ -16971,14 +16965,14 @@ snapshots:
tunnel@0.0.6: {}
- turbo@2.9.14:
+ turbo@2.9.16:
optionalDependencies:
- '@turbo/darwin-64': 2.9.14
- '@turbo/darwin-arm64': 2.9.14
- '@turbo/linux-64': 2.9.14
- '@turbo/linux-arm64': 2.9.14
- '@turbo/windows-64': 2.9.14
- '@turbo/windows-arm64': 2.9.14
+ '@turbo/darwin-64': 2.9.16
+ '@turbo/darwin-arm64': 2.9.16
+ '@turbo/linux-64': 2.9.16
+ '@turbo/linux-arm64': 2.9.16
+ '@turbo/windows-64': 2.9.16
+ '@turbo/windows-arm64': 2.9.16
tweetnacl@0.14.5: {}
@@ -17110,15 +17104,15 @@ snapshots:
use-deep-compare-effect@1.8.1(react@19.2.6):
dependencies:
- '@babel/runtime': 7.29.2
+ '@babel/runtime': 7.29.7
dequal: 2.0.3
react: 19.2.6
- use-intl@4.12.0(react@19.2.6):
+ use-intl@4.13.0(react@19.2.6):
dependencies:
'@formatjs/fast-memoize': 3.1.0
'@schummar/icu-type-parser': 1.21.5
- icu-minify: 4.12.0
+ icu-minify: 4.13.0
intl-messageformat: 11.1.1
react: 19.2.6
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index 70bdd8915..2368f6047 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -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
@@ -14,23 +14,24 @@ catalog:
"@dnd-kit/utilities": ^3.2.2
"@drizzle-team/brocli": ^0.12.0
"@extractus/feed-extractor": 7.2.1
+ '@gfazioli/mantine-onboarding-tour': ^4.0.1
"@gitbeaker/rest": ^43.8.0
"@homarr/gridstack": ^1.12.0
"@homarr/node-unifi": ^2.6.0
"@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,34 +44,34 @@ 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
- "@tiptap/extension-color": 3.23.6
- "@tiptap/extension-highlight": 3.23.6
- "@tiptap/extension-image": 3.23.6
- "@tiptap/extension-link": ^3.23.6
- "@tiptap/extension-placeholder": ^3.23.6
- "@tiptap/extension-table": 3.23.6
- "@tiptap/extension-table-cell": 3.23.6
- "@tiptap/extension-table-header": 3.23.6
- "@tiptap/extension-table-row": 3.23.6
- "@tiptap/extension-task-item": 3.23.6
- "@tiptap/extension-task-list": 3.23.6
- "@tiptap/extension-text-align": 3.23.6
- "@tiptap/extension-text-style": 3.23.6
- "@tiptap/extension-underline": 3.23.6
- "@tiptap/react": ^3.23.6
- "@tiptap/starter-kit": ^3.23.6
+ "@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.24.0
+ "@tiptap/extension-highlight": 3.24.0
+ "@tiptap/extension-image": 3.24.0
+ "@tiptap/extension-link": ^3.24.0
+ "@tiptap/extension-placeholder": ^3.24.0
+ "@tiptap/extension-table": 3.24.0
+ "@tiptap/extension-table-cell": 3.24.0
+ "@tiptap/extension-table-header": 3.24.0
+ "@tiptap/extension-table-row": 3.24.0
+ "@tiptap/extension-task-item": 3.24.0
+ "@tiptap/extension-task-list": 3.24.0
+ "@tiptap/extension-text-align": 3.24.0
+ "@tiptap/extension-text-style": 3.24.0
+ "@tiptap/extension-underline": 3.24.0
+ "@tiptap/react": ^3.24.0
+ "@tiptap/starter-kit": ^3.24.0
"@trpc/client": ^11.17.0
"@trpc/next": ^11.17.0
"@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 +110,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,30 +125,30 @@ 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
ldapts: 8.1.8
lodash.clonedeep: ^4.5.0
- mantine-react-table: 2.0.0-beta.9
+ mantine-react-table: npm:mantine-react-table-open@9.0.3
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 +159,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
diff --git a/static-data/contributors.json b/static-data/contributors.json
index cec043793..36bf46f0a 100644
--- a/static-data/contributors.json
+++ b/static-data/contributors.json
@@ -1 +1 @@
-[{"login":"ajnart","avatar_url":"https://avatars.githubusercontent.com/u/49837342?v=4"},{"login":"manuel-rw","avatar_url":"https://avatars.githubusercontent.com/u/30572287?v=4"},{"login":"Meierschlumpf","avatar_url":"https://avatars.githubusercontent.com/u/63781622?v=4"},{"login":"SeDemal","avatar_url":"https://avatars.githubusercontent.com/u/26098587?v=4"},{"login":"lammersbjorn","avatar_url":"https://avatars.githubusercontent.com/u/71191962?v=4"},{"login":"LarveyOfficial","avatar_url":"https://avatars.githubusercontent.com/u/39219859?v=4"},{"login":"c00ldude1oo","avatar_url":"https://avatars.githubusercontent.com/u/15677803?v=4"},{"login":"InterN0te","avatar_url":"https://avatars.githubusercontent.com/u/10882916?v=4"},{"login":"Aimsucks","avatar_url":"https://avatars.githubusercontent.com/u/1828486?v=4"},{"login":"JannesV","avatar_url":"https://avatars.githubusercontent.com/u/5984244?v=4"},{"login":"MauriceNino","avatar_url":"https://avatars.githubusercontent.com/u/18658092?v=4"},{"login":"hillaliy","avatar_url":"https://avatars.githubusercontent.com/u/950010?v=4"},{"login":"Skeeww","avatar_url":"https://avatars.githubusercontent.com/u/9639992?v=4"},{"login":"RichyHBM","avatar_url":"https://avatars.githubusercontent.com/u/5472275?v=4"},{"login":"Bonfire","avatar_url":"https://avatars.githubusercontent.com/u/5704760?v=4"},{"login":"wieli99","avatar_url":"https://avatars.githubusercontent.com/u/46794825?v=4"},{"login":"ishaanparlikar","avatar_url":"https://avatars.githubusercontent.com/u/43013132?v=4"},{"login":"Zareix","avatar_url":"https://avatars.githubusercontent.com/u/29818713?v=4"},{"login":"tuggan","avatar_url":"https://avatars.githubusercontent.com/u/4325608?v=4"},{"login":"Spillebulle","avatar_url":"https://avatars.githubusercontent.com/u/46653946?v=4"},{"login":"marektancak0106","avatar_url":"https://avatars.githubusercontent.com/u/25422667?v=4"},{"login":"spkesDE","avatar_url":"https://avatars.githubusercontent.com/u/1682314?v=4"},{"login":"j3lte","avatar_url":"https://avatars.githubusercontent.com/u/2557568?v=4"},{"login":"dslatt","avatar_url":"https://avatars.githubusercontent.com/u/5211001?v=4"},{"login":"Malong11-007","avatar_url":"https://avatars.githubusercontent.com/u/40298510?v=4"},{"login":"StefanB7","avatar_url":"https://avatars.githubusercontent.com/u/22056509?v=4"},{"login":"souravghosh01","avatar_url":"https://avatars.githubusercontent.com/u/37548139?v=4"},{"login":"SR3u","avatar_url":"https://avatars.githubusercontent.com/u/2380522?v=4"},{"login":"Rikpat","avatar_url":"https://avatars.githubusercontent.com/u/33869814?v=4"},{"login":"NormanJS","avatar_url":"https://avatars.githubusercontent.com/u/83409549?v=4"},{"login":"No-Maines-Land","avatar_url":"https://avatars.githubusercontent.com/u/20687725?v=4"},{"login":"stark1tty","avatar_url":"https://avatars.githubusercontent.com/u/69326376?v=4"},{"login":"Momcilo42","avatar_url":"https://avatars.githubusercontent.com/u/10118137?v=4"},{"login":"marius-arch","avatar_url":"https://avatars.githubusercontent.com/u/72494359?v=4"},{"login":"Lumilias","avatar_url":"https://avatars.githubusercontent.com/u/10852161?v=4"},{"login":"Havock94","avatar_url":"https://avatars.githubusercontent.com/u/7635248?v=4"},{"login":"Lollyde","avatar_url":"https://avatars.githubusercontent.com/u/11347524?v=4"},{"login":"krishnamuppaneni","avatar_url":"https://avatars.githubusercontent.com/u/4703366?v=4"},{"login":"devtobi","avatar_url":"https://avatars.githubusercontent.com/u/28538704?v=4"},{"login":"TyxTang","avatar_url":"https://avatars.githubusercontent.com/u/90897955?v=4"},{"login":"alotlikebeans","avatar_url":"https://avatars.githubusercontent.com/u/19630511?v=4"},{"login":"WillyJL","avatar_url":"https://avatars.githubusercontent.com/u/49810075?v=4"},{"login":"anonysoul","avatar_url":"https://avatars.githubusercontent.com/u/105715150?v=4"},{"login":"cospeedster","avatar_url":"https://avatars.githubusercontent.com/u/52749645?v=4"},{"login":"diederbert","avatar_url":"https://avatars.githubusercontent.com/u/162878798?v=4"},{"login":"digdilem","avatar_url":"https://avatars.githubusercontent.com/u/4052275?v=4"},{"login":"dinghino","avatar_url":"https://avatars.githubusercontent.com/u/14729459?v=4"},{"login":"gfomichev","avatar_url":"https://avatars.githubusercontent.com/u/1596146?v=4"},{"login":"gnattu","avatar_url":"https://avatars.githubusercontent.com/u/12995396?v=4"},{"login":"tuxpizza","avatar_url":"https://avatars.githubusercontent.com/u/84710786?v=4"},{"login":"xmarkurion","avatar_url":"https://avatars.githubusercontent.com/u/46654687?v=4"},{"login":"huanga","avatar_url":"https://avatars.githubusercontent.com/u/732970?v=4"},{"login":"taos15","avatar_url":"https://avatars.githubusercontent.com/u/122130728?v=4"},{"login":"arghyadipchak","avatar_url":"https://avatars.githubusercontent.com/u/32430374?v=4"},{"login":"AuthorShin","avatar_url":"https://avatars.githubusercontent.com/u/4959043?v=4"},{"login":"benniblot","avatar_url":"https://avatars.githubusercontent.com/u/47495914?v=4"},{"login":"bebsworthy","avatar_url":"https://avatars.githubusercontent.com/u/5266759?v=4"},{"login":"catrielmuller","avatar_url":"https://avatars.githubusercontent.com/u/2272323?v=4"},{"login":"Vysp3r","avatar_url":"https://avatars.githubusercontent.com/u/62019735?v=4"},{"login":"DR-Blogs","avatar_url":"https://avatars.githubusercontent.com/u/96393186?v=4"},{"login":"singular0","avatar_url":"https://avatars.githubusercontent.com/u/1925883?v=4"},{"login":"diogovalentte","avatar_url":"https://avatars.githubusercontent.com/u/49578155?v=4"},{"login":"codezninja","avatar_url":"https://avatars.githubusercontent.com/u/4400773?v=4"},{"login":"fabricionaweb","avatar_url":"https://avatars.githubusercontent.com/u/15933?v=4"},{"login":"nyok1912","avatar_url":"https://avatars.githubusercontent.com/u/32163949?v=4"},{"login":"bo3bdo","avatar_url":"https://avatars.githubusercontent.com/u/5271380?v=4"},{"login":"hskrtich","avatar_url":"https://avatars.githubusercontent.com/u/1214484?v=4"},{"login":"ishults","avatar_url":"https://avatars.githubusercontent.com/u/7275442?v=4"},{"login":"jelliuk","avatar_url":"https://avatars.githubusercontent.com/u/4790959?v=4"},{"login":"Moohan","avatar_url":"https://avatars.githubusercontent.com/u/5982260?v=4"},{"login":"tesar-tech","avatar_url":"https://avatars.githubusercontent.com/u/33880579?v=4"},{"login":"jbruell","avatar_url":"https://avatars.githubusercontent.com/u/31685172?v=4"},{"login":"JasonLeeB06","avatar_url":"https://avatars.githubusercontent.com/u/82266978?v=4"},{"login":"jeffersonraimon","avatar_url":"https://avatars.githubusercontent.com/u/80064475?v=4"},{"login":"jo-chemla","avatar_url":"https://avatars.githubusercontent.com/u/16822841?v=4"},{"login":"joser93","avatar_url":"https://avatars.githubusercontent.com/u/6912089?v=4"},{"login":"justijndepover","avatar_url":"https://avatars.githubusercontent.com/u/9008623?v=4"},{"login":"Aandree5","avatar_url":"https://avatars.githubusercontent.com/u/32734153?v=4"},{"login":"jackwellerreal","avatar_url":"https://avatars.githubusercontent.com/u/97435840?v=4"},{"login":"mvanhorn","avatar_url":"https://avatars.githubusercontent.com/u/455140?v=4"},{"login":"SergioChan","avatar_url":"https://avatars.githubusercontent.com/u/10103766?v=4"},{"login":"toineenzo","avatar_url":"https://avatars.githubusercontent.com/u/4921397?v=4"},{"login":"oben01","avatar_url":"https://avatars.githubusercontent.com/u/22369027?v=4"},{"login":"rezstje","avatar_url":"https://avatars.githubusercontent.com/u/45420315?v=4"},{"login":"castielwaverly","avatar_url":"https://avatars.githubusercontent.com/u/137005689?v=4"},{"login":"xoxyuxu","avatar_url":"https://avatars.githubusercontent.com/u/3543416?v=4"},{"login":"StrandedTurtle","avatar_url":"https://avatars.githubusercontent.com/u/132835255?v=4"},{"login":"MaienM","avatar_url":"https://avatars.githubusercontent.com/u/583129?v=4"},{"login":"kylegrieder","avatar_url":"https://avatars.githubusercontent.com/u/25424229?v=4"},{"login":"HeapReaper","avatar_url":"https://avatars.githubusercontent.com/u/31496522?v=4"},{"login":"Copilot","avatar_url":"https://avatars.githubusercontent.com/in/1143301?v=4"},{"login":"Gilmoursa","avatar_url":"https://avatars.githubusercontent.com/u/10321421?v=4"},{"login":"AartSchinkel","avatar_url":"https://avatars.githubusercontent.com/u/189675191?v=4"},{"login":"mairas","avatar_url":"https://avatars.githubusercontent.com/u/407010?v=4"},{"login":"itzTheMeow","avatar_url":"https://avatars.githubusercontent.com/u/50887230?v=4"},{"login":"NicolasNewman","avatar_url":"https://avatars.githubusercontent.com/u/16135852?v=4"},{"login":"NikeLaosClericus","avatar_url":"https://avatars.githubusercontent.com/u/39810627?v=4"},{"login":"noahcroghan","avatar_url":"https://avatars.githubusercontent.com/u/27422774?v=4"},{"login":"ohmzi","avatar_url":"https://avatars.githubusercontent.com/u/6551272?v=4"},{"login":"Rhiz3K","avatar_url":"https://avatars.githubusercontent.com/u/33246262?v=4"},{"login":"saschabuehrle","avatar_url":"https://avatars.githubusercontent.com/u/47737812?v=4"},{"login":"CrazyWolf13","avatar_url":"https://avatars.githubusercontent.com/u/96661824?v=4"},{"login":"VIVAAN-DHAWAN","avatar_url":"https://avatars.githubusercontent.com/u/150339722?v=4"},{"login":"generalmat82","avatar_url":"https://avatars.githubusercontent.com/u/110256073?v=4"},{"login":"lthomsen","avatar_url":"https://avatars.githubusercontent.com/u/28560325?v=4"},{"login":"pitschi","avatar_url":"https://avatars.githubusercontent.com/u/6841565?v=4"},{"login":"sharpsalat","avatar_url":"https://avatars.githubusercontent.com/u/15666932?v=4"},{"login":"tech62","avatar_url":"https://avatars.githubusercontent.com/u/16237642?v=4"},{"login":"m0s4ik","avatar_url":"https://avatars.githubusercontent.com/u/26578395?v=4"},{"login":"ashtonbatty","avatar_url":"https://avatars.githubusercontent.com/u/230635?v=4"},{"login":"Oupsman","avatar_url":"https://avatars.githubusercontent.com/u/11260343?v=4"},{"login":"cethien","avatar_url":"https://avatars.githubusercontent.com/u/60463918?v=4"},{"login":"DZ945brH","avatar_url":"https://avatars.githubusercontent.com/u/42635243?v=4"},{"login":"dgshue","avatar_url":"https://avatars.githubusercontent.com/u/12952156?v=4"},{"login":"d-lindahl","avatar_url":"https://avatars.githubusercontent.com/u/690410?v=4"},{"login":"KartulUdus","avatar_url":"https://avatars.githubusercontent.com/u/23346150?v=4"},{"login":"ieugen","avatar_url":"https://avatars.githubusercontent.com/u/1050920?v=4"},{"login":"faugusto-oliveira","avatar_url":"https://avatars.githubusercontent.com/u/37384074?v=4"},{"login":"frankvHoof93","avatar_url":"https://avatars.githubusercontent.com/u/7346516?v=4"},{"login":"febrile42","avatar_url":"https://avatars.githubusercontent.com/u/14597122?v=4"},{"login":"Kaotic3","avatar_url":"https://avatars.githubusercontent.com/u/80522225?v=4"},{"login":"KelianB","avatar_url":"https://avatars.githubusercontent.com/u/11216155?v=4"},{"login":"hotrungnhan","avatar_url":"https://avatars.githubusercontent.com/u/40029631?v=4"},{"login":"leandroscardua","avatar_url":"https://avatars.githubusercontent.com/u/24698541?v=4"}]
\ No newline at end of file
+[{"login":"ajnart","avatar_url":"https://avatars.githubusercontent.com/u/49837342?v=4"},{"login":"manuel-rw","avatar_url":"https://avatars.githubusercontent.com/u/30572287?v=4"},{"login":"Meierschlumpf","avatar_url":"https://avatars.githubusercontent.com/u/63781622?v=4"},{"login":"SeDemal","avatar_url":"https://avatars.githubusercontent.com/u/26098587?v=4"},{"login":"lammersbjorn","avatar_url":"https://avatars.githubusercontent.com/u/71191962?v=4"},{"login":"LarveyOfficial","avatar_url":"https://avatars.githubusercontent.com/u/39219859?v=4"},{"login":"c00ldude1oo","avatar_url":"https://avatars.githubusercontent.com/u/15677803?v=4"},{"login":"InterN0te","avatar_url":"https://avatars.githubusercontent.com/u/10882916?v=4"},{"login":"Aimsucks","avatar_url":"https://avatars.githubusercontent.com/u/1828486?v=4"},{"login":"JannesV","avatar_url":"https://avatars.githubusercontent.com/u/5984244?v=4"},{"login":"MauriceNino","avatar_url":"https://avatars.githubusercontent.com/u/18658092?v=4"},{"login":"hillaliy","avatar_url":"https://avatars.githubusercontent.com/u/950010?v=4"},{"login":"Skeeww","avatar_url":"https://avatars.githubusercontent.com/u/9639992?v=4"},{"login":"RichyHBM","avatar_url":"https://avatars.githubusercontent.com/u/5472275?v=4"},{"login":"Bonfire","avatar_url":"https://avatars.githubusercontent.com/u/5704760?v=4"},{"login":"wieli99","avatar_url":"https://avatars.githubusercontent.com/u/46794825?v=4"},{"login":"ishaanparlikar","avatar_url":"https://avatars.githubusercontent.com/u/43013132?v=4"},{"login":"Zareix","avatar_url":"https://avatars.githubusercontent.com/u/29818713?v=4"},{"login":"tuggan","avatar_url":"https://avatars.githubusercontent.com/u/4325608?v=4"},{"login":"Spillebulle","avatar_url":"https://avatars.githubusercontent.com/u/46653946?v=4"},{"login":"marektancak0106","avatar_url":"https://avatars.githubusercontent.com/u/25422667?v=4"},{"login":"spkesDE","avatar_url":"https://avatars.githubusercontent.com/u/1682314?v=4"},{"login":"j3lte","avatar_url":"https://avatars.githubusercontent.com/u/2557568?v=4"},{"login":"dslatt","avatar_url":"https://avatars.githubusercontent.com/u/5211001?v=4"},{"login":"Malong11-007","avatar_url":"https://avatars.githubusercontent.com/u/40298510?v=4"},{"login":"StefanB7","avatar_url":"https://avatars.githubusercontent.com/u/22056509?v=4"},{"login":"souravghosh01","avatar_url":"https://avatars.githubusercontent.com/u/37548139?v=4"},{"login":"SR3u","avatar_url":"https://avatars.githubusercontent.com/u/2380522?v=4"},{"login":"Rikpat","avatar_url":"https://avatars.githubusercontent.com/u/33869814?v=4"},{"login":"NormanJS","avatar_url":"https://avatars.githubusercontent.com/u/83409549?v=4"},{"login":"No-Maines-Land","avatar_url":"https://avatars.githubusercontent.com/u/20687725?v=4"},{"login":"stark1tty","avatar_url":"https://avatars.githubusercontent.com/u/69326376?v=4"},{"login":"Momcilo42","avatar_url":"https://avatars.githubusercontent.com/u/10118137?v=4"},{"login":"marius-arch","avatar_url":"https://avatars.githubusercontent.com/u/72494359?v=4"},{"login":"Lumilias","avatar_url":"https://avatars.githubusercontent.com/u/10852161?v=4"},{"login":"Havock94","avatar_url":"https://avatars.githubusercontent.com/u/7635248?v=4"},{"login":"Lollyde","avatar_url":"https://avatars.githubusercontent.com/u/11347524?v=4"},{"login":"krishnamuppaneni","avatar_url":"https://avatars.githubusercontent.com/u/4703366?v=4"},{"login":"devtobi","avatar_url":"https://avatars.githubusercontent.com/u/28538704?v=4"},{"login":"TyxTang","avatar_url":"https://avatars.githubusercontent.com/u/90897955?v=4"},{"login":"alotlikebeans","avatar_url":"https://avatars.githubusercontent.com/u/19630511?v=4"},{"login":"WillyJL","avatar_url":"https://avatars.githubusercontent.com/u/49810075?v=4"},{"login":"anonysoul","avatar_url":"https://avatars.githubusercontent.com/u/105715150?v=4"},{"login":"cospeedster","avatar_url":"https://avatars.githubusercontent.com/u/52749645?v=4"},{"login":"diederbert","avatar_url":"https://avatars.githubusercontent.com/u/162878798?v=4"},{"login":"digdilem","avatar_url":"https://avatars.githubusercontent.com/u/4052275?v=4"},{"login":"dinghino","avatar_url":"https://avatars.githubusercontent.com/u/14729459?v=4"},{"login":"gfomichev","avatar_url":"https://avatars.githubusercontent.com/u/1596146?v=4"},{"login":"gnattu","avatar_url":"https://avatars.githubusercontent.com/u/12995396?v=4"},{"login":"tuxpizza","avatar_url":"https://avatars.githubusercontent.com/u/84710786?v=4"},{"login":"xmarkurion","avatar_url":"https://avatars.githubusercontent.com/u/46654687?v=4"},{"login":"huanga","avatar_url":"https://avatars.githubusercontent.com/u/732970?v=4"},{"login":"taos15","avatar_url":"https://avatars.githubusercontent.com/u/122130728?v=4"},{"login":"arghyadipchak","avatar_url":"https://avatars.githubusercontent.com/u/32430374?v=4"},{"login":"AuthorShin","avatar_url":"https://avatars.githubusercontent.com/u/4959043?v=4"},{"login":"benniblot","avatar_url":"https://avatars.githubusercontent.com/u/47495914?v=4"},{"login":"bebsworthy","avatar_url":"https://avatars.githubusercontent.com/u/5266759?v=4"},{"login":"catrielmuller","avatar_url":"https://avatars.githubusercontent.com/u/2272323?v=4"},{"login":"Vysp3r","avatar_url":"https://avatars.githubusercontent.com/u/62019735?v=4"},{"login":"DR-Blogs","avatar_url":"https://avatars.githubusercontent.com/u/96393186?v=4"},{"login":"singular0","avatar_url":"https://avatars.githubusercontent.com/u/1925883?v=4"},{"login":"diogovalentte","avatar_url":"https://avatars.githubusercontent.com/u/49578155?v=4"},{"login":"codezninja","avatar_url":"https://avatars.githubusercontent.com/u/4400773?v=4"},{"login":"fabricionaweb","avatar_url":"https://avatars.githubusercontent.com/u/15933?v=4"},{"login":"nyok1912","avatar_url":"https://avatars.githubusercontent.com/u/32163949?v=4"},{"login":"bo3bdo","avatar_url":"https://avatars.githubusercontent.com/u/5271380?v=4"},{"login":"hskrtich","avatar_url":"https://avatars.githubusercontent.com/u/1214484?v=4"},{"login":"ishults","avatar_url":"https://avatars.githubusercontent.com/u/7275442?v=4"},{"login":"jelliuk","avatar_url":"https://avatars.githubusercontent.com/u/4790959?v=4"},{"login":"Moohan","avatar_url":"https://avatars.githubusercontent.com/u/5982260?v=4"},{"login":"tesar-tech","avatar_url":"https://avatars.githubusercontent.com/u/33880579?v=4"},{"login":"jbruell","avatar_url":"https://avatars.githubusercontent.com/u/31685172?v=4"},{"login":"JasonLeeB06","avatar_url":"https://avatars.githubusercontent.com/u/82266978?v=4"},{"login":"jeffersonraimon","avatar_url":"https://avatars.githubusercontent.com/u/80064475?v=4"},{"login":"jo-chemla","avatar_url":"https://avatars.githubusercontent.com/u/16822841?v=4"},{"login":"joser93","avatar_url":"https://avatars.githubusercontent.com/u/6912089?v=4"},{"login":"justijndepover","avatar_url":"https://avatars.githubusercontent.com/u/9008623?v=4"},{"login":"Aandree5","avatar_url":"https://avatars.githubusercontent.com/u/32734153?v=4"},{"login":"jackwellerreal","avatar_url":"https://avatars.githubusercontent.com/u/97435840?v=4"},{"login":"mvanhorn","avatar_url":"https://avatars.githubusercontent.com/u/455140?v=4"},{"login":"SergioChan","avatar_url":"https://avatars.githubusercontent.com/u/10103766?v=4"},{"login":"toineenzo","avatar_url":"https://avatars.githubusercontent.com/u/4921397?v=4"},{"login":"oben01","avatar_url":"https://avatars.githubusercontent.com/u/22369027?v=4"},{"login":"rezstje","avatar_url":"https://avatars.githubusercontent.com/u/45420315?v=4"},{"login":"castielwaverly","avatar_url":"https://avatars.githubusercontent.com/u/137005689?v=4"},{"login":"xoxyuxu","avatar_url":"https://avatars.githubusercontent.com/u/3543416?v=4"},{"login":"StrandedTurtle","avatar_url":"https://avatars.githubusercontent.com/u/132835255?v=4"},{"login":"MaienM","avatar_url":"https://avatars.githubusercontent.com/u/583129?v=4"},{"login":"kylegrieder","avatar_url":"https://avatars.githubusercontent.com/u/25424229?v=4"},{"login":"HeapReaper","avatar_url":"https://avatars.githubusercontent.com/u/31496522?v=4"},{"login":"Copilot","avatar_url":"https://avatars.githubusercontent.com/in/1143301?v=4"},{"login":"Gilmoursa","avatar_url":"https://avatars.githubusercontent.com/u/10321421?v=4"},{"login":"leandroscardua","avatar_url":"https://avatars.githubusercontent.com/u/24698541?v=4"},{"login":"mairas","avatar_url":"https://avatars.githubusercontent.com/u/407010?v=4"},{"login":"itzTheMeow","avatar_url":"https://avatars.githubusercontent.com/u/50887230?v=4"},{"login":"NicolasNewman","avatar_url":"https://avatars.githubusercontent.com/u/16135852?v=4"},{"login":"NikeLaosClericus","avatar_url":"https://avatars.githubusercontent.com/u/39810627?v=4"},{"login":"noahcroghan","avatar_url":"https://avatars.githubusercontent.com/u/27422774?v=4"},{"login":"ohmzi","avatar_url":"https://avatars.githubusercontent.com/u/6551272?v=4"},{"login":"Rhiz3K","avatar_url":"https://avatars.githubusercontent.com/u/33246262?v=4"},{"login":"saschabuehrle","avatar_url":"https://avatars.githubusercontent.com/u/47737812?v=4"},{"login":"CrazyWolf13","avatar_url":"https://avatars.githubusercontent.com/u/96661824?v=4"},{"login":"VIVAAN-DHAWAN","avatar_url":"https://avatars.githubusercontent.com/u/150339722?v=4"},{"login":"generalmat82","avatar_url":"https://avatars.githubusercontent.com/u/110256073?v=4"},{"login":"lthomsen","avatar_url":"https://avatars.githubusercontent.com/u/28560325?v=4"},{"login":"pitschi","avatar_url":"https://avatars.githubusercontent.com/u/6841565?v=4"},{"login":"sharpsalat","avatar_url":"https://avatars.githubusercontent.com/u/15666932?v=4"},{"login":"tech62","avatar_url":"https://avatars.githubusercontent.com/u/16237642?v=4"},{"login":"AartSchinkel","avatar_url":"https://avatars.githubusercontent.com/u/189675191?v=4"},{"login":"afonsojramos","avatar_url":"https://avatars.githubusercontent.com/u/19473034?v=4"},{"login":"m0s4ik","avatar_url":"https://avatars.githubusercontent.com/u/26578395?v=4"},{"login":"ashtonbatty","avatar_url":"https://avatars.githubusercontent.com/u/230635?v=4"},{"login":"Oupsman","avatar_url":"https://avatars.githubusercontent.com/u/11260343?v=4"},{"login":"cethien","avatar_url":"https://avatars.githubusercontent.com/u/60463918?v=4"},{"login":"DZ945brH","avatar_url":"https://avatars.githubusercontent.com/u/42635243?v=4"},{"login":"dgshue","avatar_url":"https://avatars.githubusercontent.com/u/12952156?v=4"},{"login":"d-lindahl","avatar_url":"https://avatars.githubusercontent.com/u/690410?v=4"},{"login":"KartulUdus","avatar_url":"https://avatars.githubusercontent.com/u/23346150?v=4"},{"login":"ieugen","avatar_url":"https://avatars.githubusercontent.com/u/1050920?v=4"},{"login":"faugusto-oliveira","avatar_url":"https://avatars.githubusercontent.com/u/37384074?v=4"},{"login":"frankvHoof93","avatar_url":"https://avatars.githubusercontent.com/u/7346516?v=4"},{"login":"febrile42","avatar_url":"https://avatars.githubusercontent.com/u/14597122?v=4"},{"login":"Kaotic3","avatar_url":"https://avatars.githubusercontent.com/u/80522225?v=4"},{"login":"KelianB","avatar_url":"https://avatars.githubusercontent.com/u/11216155?v=4"},{"login":"hotrungnhan","avatar_url":"https://avatars.githubusercontent.com/u/40029631?v=4"}]
\ No newline at end of file
diff --git a/static-data/translators.json b/static-data/translators.json
index 2c8152be5..5818e6339 100644
--- a/static-data/translators.json
+++ b/static-data/translators.json
@@ -1 +1 @@
-[{"username":"carlchina","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/6697/medium/d22bbe7797bbeb30dbdc73a5648d329a_default.png"},{"username":"zielmann","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/7795/medium/ad22b8b8d5eb33e4154d53a454c862fd_default.png"},{"username":"Thalyn","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12494028/medium/5faf5acc945a874ed116c4567edaa09f_default.png"},{"username":"magnushj","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12503488/medium/68f32e33ea7b891960055c9f831f7344_default.png"},{"username":"almontegil","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12580457/medium/f4136cacbdfdb4c28ae7f85dc5f840db_default.png"},{"username":"andibing","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12664938/medium/b8be63e4dcb2e791ced1ffc9e3a049a5.jpg"},{"username":"gronare","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12680911/medium/c6eb14d1504156f34a158e507f73aa3d_default.png"},{"username":"SmartPhoneLover","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12701640/medium/ec95486662ec875cda080e778c3ff702.jpg"},{"username":"marcelotk_15","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12773227/medium/ecf6bea16ab0c873c131df2154ee9718_default.png"},{"username":"S3OD177","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13051544/medium/11dd990837f61f7e8f7d36b041a7fddb_default.png"},{"username":"marcg","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13153301/medium/ebcf60fbf27deb148ec40a659566c6ff_default.png"},{"username":"BeersTeddy","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13185230/medium/2f1f4e1effe74a23422b195cbefb2a95_default.png"},{"username":"jamesmcmahon0","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13245578/medium/586aa873b4abddbd9abc6f3de99ab70e.jpeg"},{"username":"b1nar10","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13286752/medium/249b02e3368752cb175446082015737c_default.png"},{"username":"vannCN","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13330448/medium/9b8a9ee3611e51e951e22d5fd4eb7d8d.jpg"},{"username":"binswm","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13343482/medium/45dde0e6097b9b72705d2eba9dbbc276_default.png"},{"username":"SkewRam","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13496556/medium/188f5c2deb7938eda51eb786cc4539ca.jpeg"},{"username":"raphcatarino","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13547726/medium/b003511e67df13a4b4b5689488fa8099.jpg"},{"username":"shillos5","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13600557/medium/b17650b2b84f829d54fa548c44c1ab27.jpeg"},{"username":"wolong98","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13641407/medium/f4634edc58c7857a357e5293543c15cf.jpg"},{"username":"spair0039","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14012333/medium/72430e96027c09c19141cac38eae4617.png"},{"username":"MajorMarcin","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14072183/medium/0a197e64bb92b6ea94cecbb607512c45.jpg"},{"username":"KosmoMoustache","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14103535/medium/51c438cdd9f86e8811c75f4869de479c.png"},{"username":"Soochaehwa","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14118689/medium/496a1ce63111547bf455a1e0a7ac75f1_default.png"},{"username":"garryfield","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14187035/medium/520c964706676045f6eeec1b08edc880.jpg"},{"username":"droidenko","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14444264/medium/4aa3a8e824f72bc9e5ec0d8de307095e.jpeg"},{"username":"maathis","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14489950/medium/6eeddf13a757ee35f78f1763f94d95e1.jpeg"},{"username":"bukvam","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14546502/medium/a819dc40a13c57395a17dc46e847d3ce.jpg"},{"username":"gm.cinalli","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14670666/medium/2d466a0fdbda40764526be86c97c0ab4.jpeg"},{"username":"antoine2tt","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14722148/medium/f88d926900862dd59007ea4b3419cb9d.png"},{"username":"cretzen","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14799754/medium/3ba5c1805ec20c000d5810c07985afe9.jpg"},{"username":"ktKongTong","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14817246/medium/97cfc3c028dbdaf85ebd1102da71e58c.jpeg"},{"username":"marinkaberg","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14861042/medium/2d5c4e62613f03082f3e645fa92efd59.jpeg"},{"username":"djismgaming","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14934947/medium/f5a8570713c34ab0f7d5405d105e2a9a.jpeg"},{"username":"f1refa11","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14949159/medium/fd2ae63b8eb4462200ba96abf943c1b9.png"},{"username":"jeffersonraimon","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15057621/medium/6dff469563860e2618bc9d45646d0ab4.png"},{"username":"giop98","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15057987/medium/b8a4825d3fc39fc662f35ee258db4b2d.jpeg"},{"username":"tomislav.kraljevic","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15118857/medium/e133f1061cc92850b854d05d8faaeafd.png"},{"username":"Walkx","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15202182/medium/5c37361ae45aeed487b34582c1f7ca37.png"},{"username":"andrea.rosso","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15246318/medium/11f3f5ef44ec7f55b6f143090e208704_default.png"},{"username":"ajnart","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15270132/medium/a0f107a463c8910ee96bc2fa843a17e3.jpeg"},{"username":"Witchling","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15282238/medium/9e196b6783415b10ca571b7673c2ef33.jpg"},{"username":"guineuu","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15292058/medium/22fbb758bda3b7805d50bf21d38f2c20.jpeg"},{"username":"Bulgus","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15304568/medium/0e8787e5ceb02ed5c96a514d0068ae87.jpg"},{"username":"Narno","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15314996/medium/a6ba093f24884250a1bf6c6f2ca50f1b_default.png"},{"username":"BySempron","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15315986/medium/e3d22d7b1423c6823a9f36d595ed4bdb.png"},{"username":"jonathan.berglin.work","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15359236/medium/95930b2093db13b76179782f7322c5d5.png"},{"username":"icdmkg","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15405504/medium/f334d16cde1ac4ffb935b440d03465af.gif"},{"username":"irithys","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15405614/medium/3086461c47cce0a0c031925e5f943412.png"},{"username":"JannesV","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15419912/medium/a7809eb4b817d7c49b62cf10ae86b950.png"},{"username":"benniblot","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15419914/medium/8086b1573d31f152ba41949b3a172445.jpeg"},{"username":"pacjo","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15419916/medium/3cbeddbd7bc01faafb5a3bf47bba915b_default.png"},{"username":"Payou6994","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15419934/medium/37c9b9b17dfb578404c1c1ddb73ba7a8.png"},{"username":"fzibi21","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15419976/medium/0da688450358e0290a7b7359cc1f7328.png"},{"username":"WowMurdock","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15420118/medium/de19576fccb495b6dfe4c4c04a56b834.png"},{"username":"hbooo","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15420120/medium/6c7c7f8db785061356ebb03d044d3329.jpeg"},{"username":"Manicraft1001","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15420178/medium/50ec94563a06a9f74f33bd09f01eed4d.jpg"},{"username":"Void123","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15420354/medium/86929d44df92a00f9fe900a985c196df_default.png"},{"username":"R4cc","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15422606/medium/a390979662b84694f59de30bdb732141.jpeg"},{"username":"fabricionaweb","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15425808/medium/a9354142e7ae5152c144409d55fda551_default.png"},{"username":"JokeOfDead","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15426890/medium/a383eac9365e9de64fd4ab1d6fd0cb95.jpeg"},{"username":"hkz","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15427174/medium/c88acefb0d7306e1f7470e872029fb39_default.png"},{"username":"Steken","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15428516/medium/d5aea5653c769c3a523182bdb60d1664.png"},{"username":"flar.anton","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15428592/medium/b01fdb365d892e9f811f77fcb50a80a2.jpeg"},{"username":"Bon","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15433542/medium/5397da4dfc821f20b6ac14fe0c514e9a.jpeg"},{"username":"bfkadan","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15434162/medium/4f9a0b43cfe3acaea60124c14ba7f44a.png"},{"username":"BunnySweety","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15434662/medium/f0ef200a6a0dcf0e1d0e9ecd4148f560_default.png"},{"username":"HRKings","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15436168/medium/69a31e461d38549f01864e7ef10d642e.png"},{"username":"wiston81","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15439078/medium/10f292c8d12a7c21a44b54495fa4a3d8.jpeg"},{"username":"qaz0911","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15440860/medium/3d3e50ee388c72dc4bf7a771761f2d89_default.png"},{"username":"qqyule","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15441462/medium/7a3cdf82710ffb5d8f388bc0bd010665.png"},{"username":"Bims0n","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15445560/medium/603220b603eeb3367e0f0d3fa675247c.jpg"},{"username":"TariqDaCoder","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15446228/medium/50b0f4040112bbd67690b769477398e5_default.png"},{"username":"Anarchon","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15449644/medium/e925e1f3e3ffbf0f982391ce263a1a28.jpeg"},{"username":"Ashun","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15453020/medium/ccdcf51c73d6aae40751bb30beee1915_default.png"},{"username":"sebekmartin","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15454038/medium/bcfb44598cdfd1d7cd4eb35812538962.jpeg"},{"username":"RagnarGraves","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15459882/medium/143d5af850c1154070a218bea124e9cb_default.png"},{"username":"PrtmPhlp","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15462414/medium/b80db55e9de301432dcd1f8c8b24fd49_default.png"},{"username":"DooYoo","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15470768/medium/2a18cf4be67094724b508c9e1e698a21_default.png"},{"username":"espentruls","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15486092/medium/8e38afc3a4ff669226a0cfd3e420ff3a.jpeg"},{"username":"frisco82","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15486922/medium/4e1c5d4189b42508e660daa3c1c25b2a.jpeg"},{"username":"lupineDK","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15491798/medium/461bd501e8371c062bf29ea171aedd36_default.png"},{"username":"hillaliy","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15492732/medium/0bae17b421604892d888e3fc70cf0587.jpeg"},{"username":"MarcOrfilaCarreras","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15501072/medium/ea52b26c3c6f21e4931e38e3ce3f3d6e.png"},{"username":"robertbridda","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15507822/medium/a368c2e30411bb2da9b49290084191f3.png"},{"username":"MKoniuszko","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15518090/medium/87605434fcc839f6763ab07c50f6d232.jpeg"},{"username":"HeroSizy","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15518710/medium/4e79c0e98cbeb536dd961e656331b509.png"},{"username":"dwt136","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15520022/medium/554422503f2baea43ace85facb4546fb_default.png"},{"username":"asifthewebguy","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15526719/medium/b18931dd0c800d725048bd440646198b_default.png"},{"username":"eiloogs","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15545537/medium/f290a2f1190983530a9b76b2e858a609.gif"},{"username":"_vytdv","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15547289/medium/60d8644cc8ad6f11a92ccea4a14cd098_default.png"},{"username":"crendasien","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15554645/medium/598ab1d4aaf6b8dccd5ba16be92da7b9.jpeg"},{"username":"edxo","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15573823/medium/8a565b732a75a77f840dd123cdb30bf4.png"},{"username":"somerlev","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15617065/medium/f4b13513e311ec902d90b2f718412c55.jpg"},{"username":"kid1412621","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15643771/medium/dd455e32de652fa88e6fd97598bdffa7.png"},{"username":"suming","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15644717/medium/244159dfe10fa03436205506f80c9e25.png"},{"username":"nick.gher","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15647517/medium/5374a2c6fef60a3fbf0edf86b997c351_default.png"},{"username":"DataCat","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15650315/medium/ce7c1365adf35c5d490d77500a4607fb_default.png"},{"username":"dizo89","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15658375/medium/0ca745e5017d491fe1b22b0239904de8.jpeg"},{"username":"tee_noodle","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15674577/medium/804d9cf06f5196026acb6436b809d0da_default.png"},{"username":"Marty88","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15674593/medium/492b1509d52bd2809dea768121217125.jpeg"},{"username":"Spillebulle","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15677023/medium/20b98bb85062e07afe0e63618f8a5feb.PNG"},{"username":"petitmewen","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15685239/medium/15de9b62d2e0bc25013435f1784bbcc1.jpeg"},{"username":"NoProsNoNoobs","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15687709/medium/ae8f65fafeb8bcf74dcb8871bbe46461.png"},{"username":"y.gybson","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15690777/medium/b5cb5d2d5768904ff6586c727e3a6c77.jpeg"},{"username":"inside90","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15704947/medium/c1355fcb30dd76f8e39d98d1d49f1c52.png"},{"username":"RJSkudra","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15709853/medium/c3abf2774913dc4e81fb261d36d7668c.png"},{"username":"binge203","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15713937/medium/77c1cfa4314673db80e6881fd9f64668.gif"},{"username":"Mailootje","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15714337/medium/743c3bc4ab1989966a375eeeec83d8b8.jpeg"},{"username":"tim-wiegers","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15719805/medium/08a96f064813350661cd6b20bf3d7d99.png"},{"username":"GkhnG","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15722911/medium/71a027caec489ef6ce82bcf1888329d0_default.png"},{"username":"Zoen-Millo","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15760967/medium/9e956f11adc5b34f5636268b5c485dbf.jpg"},{"username":"HooinKyoma","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15792897/medium/9489f0a9b368e0e827ae758b740a2eed.jpeg"},{"username":"wathergeen","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15794495/medium/a83b7fc80d180669ebfd6deb3544cfd0.png"},{"username":"MoeToo","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15818233/medium/d35cd6953717706eaf20f6c143c62947.png"},{"username":"itodouble","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15836233/medium/e984caea18fb0673bf319fcf28cef649.png"},{"username":"loslocitos","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15847901/medium/fe30d12fd2cf38212f929e13b169f9ec.jpeg"},{"username":"Oversleep","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15849065/medium/06141f13a6d541d753f3c2f2947b8068_default.png"},{"username":"Beardy","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15865139/medium/fca6b9d2b3f52e286d1568f52b83b6a0_default.png"},{"username":"raelyan","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15875457/medium/2f4fda1d1aaa5dcc79b328baf3f03151.jpeg"},{"username":"dolphin738","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15913763/medium/ee6fede7b8528ca642329ada80d1cc18.png"},{"username":"brunotco","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15916719/medium/09db45880fc05abc18adb8d932a5ecf9_default.png"},{"username":"LukeIsHereToDevelop","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15924739/medium/3e6cd3940297bb4ba7bb039c93e6f918.png"},{"username":"kennit","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15925879/medium/6b0733ad3c5949b91c55e4d8b03db8a5_default.png"},{"username":"kuunpire","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15950309/medium/8192a4f08f07086828ac9f74ed29a169.jpeg"},{"username":"Sandor-dev","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15951759/medium/0216c2da4eb028164ebbecf1c72f6271_default.png"},{"username":"Meierschlumpf","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15953187/medium/23c744faef1ab84fbdc9351a7850aab6.jpeg"},{"username":"harmlesscat","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15970733/medium/dd8d0214a0250c932bb518b1b55e45a2_default.png"},{"username":"OrzWTF","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15976121/medium/4c4557cbff7ff7b0503455bc59c020e0.jpeg"},{"username":"tagaishi","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15977271/medium/eade504c83a5a1ff831c80a538fbdb44_default.png"},{"username":"azurite928","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15981895/medium/547ccc459ee123e78b5401c499f1022d.png"},{"username":"Ronner231","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16021342/medium/7734d550df2de5a2fec2ffff33e7024c.jpeg"},{"username":"ugyes","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16034148/medium/ed001e3f470a2dea9a8ce955b18e7bd5.png"},{"username":"rpieja","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16045554/medium/bd55baca2ef8b92502a760cc9ee7c505_default.png"},{"username":"flambyisyou","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16051620/medium/3a3bc0c90f6b95ab4ef74396a0a17beb.png"},{"username":"Topbcy","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16077170/medium/d3aed33ea56330338756cfcd89477cfe.jpeg"},{"username":"ai5d02sb","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16084674/medium/7c8119fe2a5ca71bb15f636916a42b95_default.png"},{"username":"explosiveparrot","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16097722/medium/7762f80fc1da63f5b2eb87de9d640324_default.png"},{"username":"DestroyCom","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16111544/medium/83e3d3ac5c8bfed634cd1a074854db67.png"},{"username":"neotraxo","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16116966/medium/b30df957873329019a8a90a69f6efe3a.png"},{"username":"gzxpa","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16123314/medium/487b05577310a946b0b98bc72c11213b_default.png"},{"username":"TORIK","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16124732/medium/de770890e6411860ac06e8cc2dc0abd0_default.png"},{"username":"sergio.pibot","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16138114/medium/80db802b2fcd6bbf19ea4c7413367dca.jpeg"},{"username":"karam-ajaj","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16139838/medium/7539ab958d3c85d5f67e7849483ea8f7.jpeg"},{"username":"SimonHenz97","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16151520/medium/9b94a85b536688e2d94538c384fd0a76.png"},{"username":"olsson82","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16198988/medium/eeaab4cba210c537424d4204754b604e.png"},{"username":"bo3bdo","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16222806/medium/8652f3beacb6098dde5a4cd0a29fe80d.jpeg"},{"username":"AngryPlayer04","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16245192/medium/342715274f4dc31fd4132cdae9126188.jpeg"},{"username":"ehatamm","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16245884/medium/979d5e5526fe545be9bf46db481f7182.jpeg"},{"username":"Balionelis","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16246058/medium/986d62ba363594c517d966fe9178dcbb.png"},{"username":"kwargs","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16248816/medium/19ce11f04bf1ae2637e0483399c675f9_default.png"},{"username":"lucicu","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16273892/medium/3b09e8f3bead221ef98d6a45a16851dc_default.png"},{"username":"Michael5564445","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16357688/medium/9d9026a8bf3eaccb0417da240932ebf7.png"},{"username":"Netscream","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16359198/medium/694e0c8572dc8a47e6d63da31ec5b992.png"},{"username":"belicstefan04","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16368142/medium/4742dcfd7bf7ad7e33e37ac44fba7e0f.png"},{"username":"Hamad2066","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16371478/medium/f37bbc7685b6757defdbd1494236af61.jpeg"},{"username":"3ct0pl4sm","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16399616/medium/ce796d1c40163975624c0a483e56374f.png"},{"username":"sanchez.marcell626","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16433865/medium/3ddde1314764cc2205ffdcd1ae719a91.png"},{"username":"Minoctis","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16490079/medium/91fd0849303a4eca033d660d0a7b6854.jpeg"},{"username":"homarr","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16657003/medium/3afabfd3de949a54ff5d40ad04552aba.png"},{"username":"mwgg","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16658379/medium/763dd09f5c238cc5324d4d5357985292.png"},{"username":"kiwinho","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16671855/medium/bfe3a2d497dc4748a04d02e4ae2139cb.jpeg"},{"username":"tjvg4m34r13","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16679135/medium/7a483b9b0abe0d925e54808e1cb38fa0.jpeg"},{"username":"Diegoem","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16705921/medium/29ce68ebc191fc3f65499511b12de98f_default.png"},{"username":"bouks69","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16746091/medium/b58b1fc08520de416198be9df5d093eb.jpeg"},{"username":"futhgar","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16748067/medium/b8c526b097f9aa9d687250cac3df16dc.jpeg"},{"username":"GrimSQL","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16764343/medium/fff729a76d1f8fbcdaee9bb0c0f5f3fd.png"},{"username":"chnikel","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16767047/medium/3dda2a37e03049dafefe6697bb00f75c.png"},{"username":"yuki19870616","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16767951/medium/f59c848e38382cda6c8464b0a7b9e642_default.png"},{"username":"noamdal","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16782245/medium/b4a72a61f9caf138f0322c0cb987e6d0.png"},{"username":"MalithRukshan","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16809057/medium/084d6a5b35fbbeacc70e7021197d222f.png"},{"username":"minirope19","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16930929/medium/e8cbbc26747e31c5a8959215e1dd067b.jpeg"},{"username":"prubin","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17503492/medium/96ac87644691e49c1f983f2f094e541c_default.png"},{"username":"sam-tam-sam","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17578052/medium/1628dc2954ce3ae95e0caaa2bd60ab0c.png"},{"username":"toineenzo","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12371852/medium/9f1b2bedb4794c59479cd289bdba9fab.png"},{"username":"ArcaneWater","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12421105/medium/1ac82517a6c658f6b62c48aebcb17e86_default.png"},{"username":"Ebedami","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12974801/medium/b596dea5c09454c4c828eac558389755.jpg"},{"username":"perrygamerptyt","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13117177/medium/1406c949507ecd438480473c8a1f6124.jpg"},{"username":"SergiSvK","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13320543/medium/3df3c79cc5f0c53716c559cd3fd2fc20.png"},{"username":"jeongeon","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13362856/medium/eb572f72cde5e5526c517ca3a6489850_default.png"},{"username":"MrTranCFCVN","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13439475/medium/a1789d69c85d90c667194b5d6a518c08_default.png"},{"username":"Tobost06","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13467449/medium/9227dc07f6366bc060d28304f5cb8ce7.jpeg"},{"username":"Kf637","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13530622/medium/118b00bb72d01d3229c97409637e25d1_default.png"},{"username":"Cruellest","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13610615/medium/2a74faa0947ee08d18f7946fb8602c1e.jpg"},{"username":"olavinto","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14424644/medium/37c01732bd2b7a272b421807a05ccfeb.png"},{"username":"Thario","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14491372/medium/dd5e142070802a672900121aa3f332a6_default.png"},{"username":"Wjouter","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14585620/medium/b91a51513332bdc0a0a9ecbed99ecbc3.png"},{"username":"albanobattistella","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14686194/medium/564afcfd76f5cd24751bd669cf4943aa.jpg"},{"username":"Kachelkaiser","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14763406/medium/203a781797da4a18fe0f4e7db8fd547c.png"},{"username":"regi4","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14842936/medium/f7e1d672f562a7db61ba6b4b8f4de41d.png"},{"username":"baton5129","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14891064/medium/8d438f557b82977dd02f0ead3187265f.PNG"},{"username":"meviper","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15958903/medium/209f521ce33a05831fa8d7d1b7babc20_default.png"},{"username":"GerOriki","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16268376/medium/da00e54b08cd40348231d92873a0cd0a_default.png"},{"username":"TheRedCyclops","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16486399/medium/7f39226180120d5a9efcb8e13d8aec8e_default.png"},{"username":"dymek37","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16659221/medium/d1dc6d11d7e7fc370efc5070e837ae03.png"},{"username":"B3CKDOOR","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16744975/medium/e6c7b2d45e76465d54e6efb67c83da7d.png"},{"username":"Larsluph","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16779933/medium/ed7d36f8b44013bad3a16ac0491bd0f7.png"},{"username":"FOEDI","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16781001/medium/6b4552fb72e359aa016f7a054078fac8.png"},{"username":"xmarkurion","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16805443/medium/34a20428427de5aaef99c7d140646803.png"},{"username":"gregid","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16806705/medium/f3c8f4a20436e8538929130142bbc104.png"},{"username":"Flameberg","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16820453/medium/d47be866148474c9d05fec6fbe406d88.png"},{"username":"Playfust","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16872865/medium/5f4609cc711e5fb9fa8a0b178635769c.jpeg"},{"username":"Cotignac","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16879297/medium/81ff353edf030699a159bb79cda26e84.jpg"},{"username":"LeaHun","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16916375/medium/618520f0543b1e212ebc83d66b683d4b_default.png"},{"username":"jackwellerreal","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16919717/medium/37bbfdc03893f141133ccc619593266d.png"},{"username":"renan.costaalencar","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16938427/medium/6d25b25320c5bde734118dec4af9cd5b.png"},{"username":"andrius.cim","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16967845/medium/46028fc1a0ddfaaeebcb37b39739625d_default.png"},{"username":"xjxexrxrxyx","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17041412/medium/d34d664166db6fb25579bca9ff3b50b4.jpeg"},{"username":"bilhal.fellah","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17044572/medium/d1ec6f17e4aa013e9eededc83778d2c6.png"},{"username":"the_octo","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17058060/medium/7423ab3a4cabb67f5decb2e2dfa2b675_default.png"},{"username":"BamesJond","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17075846/medium/128d7ffd40254c8e376f45678e2fc620_default.png"},{"username":"Kornikus","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17134240/medium/d7a42b2c30db46ff152699ca01f9e11a_default.png"},{"username":"xoxyuxu","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17176526/medium/108055aa24343043de852e412bec74be.png"},{"username":"cryptodntsleep","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17249408/medium/b9e15aa2b24a0dbc1e4f194aa8ed5109.png"},{"username":"yaniorg","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17261842/medium/7dbfc77f21c826845961243c44540d26.jpg"},{"username":"Vidariondr","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17262438/medium/45fbc71be02ef27296b6f423e97b6a03_default.png"},{"username":"Fastery","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17263066/medium/701b0edc59676174863ad651528bbb37.jpeg"},{"username":"stratself","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17290388/medium/57d9d8161b3fd0abf4dc5558b4a1fa05_default.png"},{"username":"wuzeqin","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17292606/medium/b9e0308410b31337e2d7aa6ed346d718_default.png"},{"username":"ChristopherBull","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17304338/medium/21a175ce596bfae81430767467908b90.jpeg"},{"username":"darkbcnlord","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17311436/medium/b271bc0b419ec3a26ddb7b63ff8ffc4b.png"},{"username":"zhoran1997","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17344028/medium/c8f450b5a8f1e9494a45dc60af7ce63e.png"},{"username":"gilalves1703","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17447768/medium/3f1de562a5c2391c88bbf8d5cf40a266.png"},{"username":"iServeCloud","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17470960/medium/11649b99d609438d66455a4d2fdc70f7.png"},{"username":"guibolzantvbox","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17474136/medium/59a576e6da538664ceb972253916fa02.jpeg"},{"username":"ivann136","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17506062/medium/67d09ada36fac5f444a1f7517e3a97bd_default.png"},{"username":"Maheshivara","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17516082/medium/576e1e6581cf969615b265de6aeff8b7.jpeg"},{"username":"brunogui81","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17517542/medium/7c0d174e810435af518fe8e0c05e151f.jpeg"},{"username":"michelwilhelm","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17542190/medium/4c6b33fd8993ff565fa75add2ce8355e.jpeg"},{"username":"dias-gxstavo","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17547376/medium/8c6b109aae8aeb651192346a39ae9e5b.jpeg"},{"username":"ToxykAuBleu","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17614030/medium/8d21e0608560172b121bd46126d827c7.jpeg"},{"username":"e.erdemir","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17617994/medium/7259a29db9941c6171a8cbbd643b92bd.png"},{"username":"sjonkreutz","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17618306/medium/7601c45d015189a863656e21348d223b.png"},{"username":"volovich0377","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17620388/medium/c929a8fbd24f96d17620fa497ade7309.png"},{"username":"Hamstio1","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17624898/medium/406e0bf40094c24a01c3064f8f5b2a82.png"},{"username":"i3laa","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17627462/medium/613a9d283ed8b69819ee181903851660.jpeg"},{"username":"bastiengrignon","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17629884/medium/ddfc3339285bd65195fcd834e512e9b7.jpeg"},{"username":"kauhee","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17644600/medium/def1b9066cf9275b4273a5a5dca7f325.png"},{"username":"hongminzheng","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17683860/medium/a764f4963912777d27a7cd98e136a2b2.png"},{"username":"d-lindahl","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17726729/medium/cbde348582fbabcfadd89a56cd5a0b40.png"}]
\ No newline at end of file
+[{"username":"carlchina","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/6697/medium/d22bbe7797bbeb30dbdc73a5648d329a_default.png"},{"username":"zielmann","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/7795/medium/ad22b8b8d5eb33e4154d53a454c862fd_default.png"},{"username":"Thalyn","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12494028/medium/5faf5acc945a874ed116c4567edaa09f_default.png"},{"username":"magnushj","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12503488/medium/68f32e33ea7b891960055c9f831f7344_default.png"},{"username":"almontegil","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12580457/medium/f4136cacbdfdb4c28ae7f85dc5f840db_default.png"},{"username":"andibing","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12664938/medium/b8be63e4dcb2e791ced1ffc9e3a049a5.jpg"},{"username":"gronare","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12680911/medium/c6eb14d1504156f34a158e507f73aa3d_default.png"},{"username":"SmartPhoneLover","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12701640/medium/ec95486662ec875cda080e778c3ff702.jpg"},{"username":"marcelotk_15","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12773227/medium/ecf6bea16ab0c873c131df2154ee9718_default.png"},{"username":"S3OD177","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13051544/medium/11dd990837f61f7e8f7d36b041a7fddb_default.png"},{"username":"marcg","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13153301/medium/ebcf60fbf27deb148ec40a659566c6ff_default.png"},{"username":"BeersTeddy","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13185230/medium/2f1f4e1effe74a23422b195cbefb2a95_default.png"},{"username":"jamesmcmahon0","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13245578/medium/586aa873b4abddbd9abc6f3de99ab70e.jpeg"},{"username":"b1nar10","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13286752/medium/249b02e3368752cb175446082015737c_default.png"},{"username":"vannCN","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13330448/medium/9b8a9ee3611e51e951e22d5fd4eb7d8d.jpg"},{"username":"binswm","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13343482/medium/45dde0e6097b9b72705d2eba9dbbc276_default.png"},{"username":"SkewRam","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13496556/medium/188f5c2deb7938eda51eb786cc4539ca.jpeg"},{"username":"raphcatarino","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13547726/medium/b003511e67df13a4b4b5689488fa8099.jpg"},{"username":"shillos5","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13600557/medium/b17650b2b84f829d54fa548c44c1ab27.jpeg"},{"username":"wolong98","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13641407/medium/f4634edc58c7857a357e5293543c15cf.jpg"},{"username":"spair0039","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14012333/medium/72430e96027c09c19141cac38eae4617.png"},{"username":"MajorMarcin","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14072183/medium/0a197e64bb92b6ea94cecbb607512c45.jpg"},{"username":"KosmoMoustache","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14103535/medium/51c438cdd9f86e8811c75f4869de479c.png"},{"username":"Soochaehwa","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14118689/medium/496a1ce63111547bf455a1e0a7ac75f1_default.png"},{"username":"garryfield","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14187035/medium/520c964706676045f6eeec1b08edc880.jpg"},{"username":"droidenko","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14444264/medium/4aa3a8e824f72bc9e5ec0d8de307095e.jpeg"},{"username":"maathis","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14489950/medium/6eeddf13a757ee35f78f1763f94d95e1.jpeg"},{"username":"bukvam","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14546502/medium/a819dc40a13c57395a17dc46e847d3ce.jpg"},{"username":"gm.cinalli","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14670666/medium/2d466a0fdbda40764526be86c97c0ab4.jpeg"},{"username":"antoine2tt","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14722148/medium/f88d926900862dd59007ea4b3419cb9d.png"},{"username":"cretzen","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14799754/medium/3ba5c1805ec20c000d5810c07985afe9.jpg"},{"username":"ktKongTong","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14817246/medium/97cfc3c028dbdaf85ebd1102da71e58c.jpeg"},{"username":"marinkaberg","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14861042/medium/2d5c4e62613f03082f3e645fa92efd59.jpeg"},{"username":"djismgaming","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14934947/medium/f5a8570713c34ab0f7d5405d105e2a9a.jpeg"},{"username":"f1refa11","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14949159/medium/fd2ae63b8eb4462200ba96abf943c1b9.png"},{"username":"jeffersonraimon","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15057621/medium/6dff469563860e2618bc9d45646d0ab4.png"},{"username":"giop98","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15057987/medium/b8a4825d3fc39fc662f35ee258db4b2d.jpeg"},{"username":"tomislav.kraljevic","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15118857/medium/e133f1061cc92850b854d05d8faaeafd.png"},{"username":"Walkx","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15202182/medium/5c37361ae45aeed487b34582c1f7ca37.png"},{"username":"andrea.rosso","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15246318/medium/11f3f5ef44ec7f55b6f143090e208704_default.png"},{"username":"ajnart","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15270132/medium/a0f107a463c8910ee96bc2fa843a17e3.jpeg"},{"username":"Witchling","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15282238/medium/9e196b6783415b10ca571b7673c2ef33.jpg"},{"username":"guineuu","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15292058/medium/22fbb758bda3b7805d50bf21d38f2c20.jpeg"},{"username":"Bulgus","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15304568/medium/0e8787e5ceb02ed5c96a514d0068ae87.jpg"},{"username":"Narno","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15314996/medium/a6ba093f24884250a1bf6c6f2ca50f1b_default.png"},{"username":"BySempron","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15315986/medium/e3d22d7b1423c6823a9f36d595ed4bdb.png"},{"username":"jonathan.berglin.work","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15359236/medium/95930b2093db13b76179782f7322c5d5.png"},{"username":"icdmkg","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15405504/medium/f334d16cde1ac4ffb935b440d03465af.gif"},{"username":"irithys","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15405614/medium/3086461c47cce0a0c031925e5f943412.png"},{"username":"JannesV","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15419912/medium/a7809eb4b817d7c49b62cf10ae86b950.png"},{"username":"benniblot","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15419914/medium/8086b1573d31f152ba41949b3a172445.jpeg"},{"username":"pacjo","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15419916/medium/3cbeddbd7bc01faafb5a3bf47bba915b_default.png"},{"username":"Payou6994","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15419934/medium/37c9b9b17dfb578404c1c1ddb73ba7a8.png"},{"username":"fzibi21","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15419976/medium/0da688450358e0290a7b7359cc1f7328.png"},{"username":"WowMurdock","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15420118/medium/de19576fccb495b6dfe4c4c04a56b834.png"},{"username":"hbooo","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15420120/medium/6c7c7f8db785061356ebb03d044d3329.jpeg"},{"username":"Manicraft1001","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15420178/medium/50ec94563a06a9f74f33bd09f01eed4d.jpg"},{"username":"Void123","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15420354/medium/86929d44df92a00f9fe900a985c196df_default.png"},{"username":"R4cc","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15422606/medium/a390979662b84694f59de30bdb732141.jpeg"},{"username":"fabricionaweb","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15425808/medium/a9354142e7ae5152c144409d55fda551_default.png"},{"username":"JokeOfDead","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15426890/medium/a383eac9365e9de64fd4ab1d6fd0cb95.jpeg"},{"username":"hkz","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15427174/medium/c88acefb0d7306e1f7470e872029fb39_default.png"},{"username":"Steken","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15428516/medium/d5aea5653c769c3a523182bdb60d1664.png"},{"username":"flar.anton","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15428592/medium/b01fdb365d892e9f811f77fcb50a80a2.jpeg"},{"username":"Bon","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15433542/medium/5397da4dfc821f20b6ac14fe0c514e9a.jpeg"},{"username":"bfkadan","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15434162/medium/4f9a0b43cfe3acaea60124c14ba7f44a.png"},{"username":"BunnySweety","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15434662/medium/f0ef200a6a0dcf0e1d0e9ecd4148f560_default.png"},{"username":"HRKings","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15436168/medium/69a31e461d38549f01864e7ef10d642e.png"},{"username":"wiston81","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15439078/medium/10f292c8d12a7c21a44b54495fa4a3d8.jpeg"},{"username":"qaz0911","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15440860/medium/3d3e50ee388c72dc4bf7a771761f2d89_default.png"},{"username":"qqyule","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15441462/medium/7a3cdf82710ffb5d8f388bc0bd010665.png"},{"username":"Bims0n","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15445560/medium/603220b603eeb3367e0f0d3fa675247c.jpg"},{"username":"TariqDaCoder","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15446228/medium/50b0f4040112bbd67690b769477398e5_default.png"},{"username":"Anarchon","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15449644/medium/e925e1f3e3ffbf0f982391ce263a1a28.jpeg"},{"username":"Ashun","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15453020/medium/ccdcf51c73d6aae40751bb30beee1915_default.png"},{"username":"sebekmartin","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15454038/medium/bcfb44598cdfd1d7cd4eb35812538962.jpeg"},{"username":"RagnarGraves","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15459882/medium/143d5af850c1154070a218bea124e9cb_default.png"},{"username":"PrtmPhlp","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15462414/medium/b80db55e9de301432dcd1f8c8b24fd49_default.png"},{"username":"DooYoo","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15470768/medium/2a18cf4be67094724b508c9e1e698a21_default.png"},{"username":"espentruls","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15486092/medium/8e38afc3a4ff669226a0cfd3e420ff3a.jpeg"},{"username":"frisco82","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15486922/medium/4e1c5d4189b42508e660daa3c1c25b2a.jpeg"},{"username":"lupineDK","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15491798/medium/461bd501e8371c062bf29ea171aedd36_default.png"},{"username":"hillaliy","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15492732/medium/0bae17b421604892d888e3fc70cf0587.jpeg"},{"username":"MarcOrfilaCarreras","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15501072/medium/ea52b26c3c6f21e4931e38e3ce3f3d6e.png"},{"username":"robertbridda","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15507822/medium/a368c2e30411bb2da9b49290084191f3.png"},{"username":"MKoniuszko","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15518090/medium/87605434fcc839f6763ab07c50f6d232.jpeg"},{"username":"HeroSizy","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15518710/medium/4e79c0e98cbeb536dd961e656331b509.png"},{"username":"dwt136","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15520022/medium/554422503f2baea43ace85facb4546fb_default.png"},{"username":"asifthewebguy","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15526719/medium/b18931dd0c800d725048bd440646198b_default.png"},{"username":"eiloogs","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15545537/medium/f290a2f1190983530a9b76b2e858a609.gif"},{"username":"_vytdv","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15547289/medium/60d8644cc8ad6f11a92ccea4a14cd098_default.png"},{"username":"crendasien","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15554645/medium/598ab1d4aaf6b8dccd5ba16be92da7b9.jpeg"},{"username":"edxo","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15573823/medium/8a565b732a75a77f840dd123cdb30bf4.png"},{"username":"somerlev","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15617065/medium/f4b13513e311ec902d90b2f718412c55.jpg"},{"username":"kid1412621","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15643771/medium/dd455e32de652fa88e6fd97598bdffa7.png"},{"username":"suming","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15644717/medium/244159dfe10fa03436205506f80c9e25.png"},{"username":"nick.gher","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15647517/medium/5374a2c6fef60a3fbf0edf86b997c351_default.png"},{"username":"DataCat","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15650315/medium/ce7c1365adf35c5d490d77500a4607fb_default.png"},{"username":"dizo89","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15658375/medium/0ca745e5017d491fe1b22b0239904de8.jpeg"},{"username":"tee_noodle","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15674577/medium/804d9cf06f5196026acb6436b809d0da_default.png"},{"username":"Marty88","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15674593/medium/492b1509d52bd2809dea768121217125.jpeg"},{"username":"Spillebulle","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15677023/medium/20b98bb85062e07afe0e63618f8a5feb.PNG"},{"username":"petitmewen","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15685239/medium/15de9b62d2e0bc25013435f1784bbcc1.jpeg"},{"username":"NoProsNoNoobs","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15687709/medium/ae8f65fafeb8bcf74dcb8871bbe46461.png"},{"username":"y.gybson","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15690777/medium/b5cb5d2d5768904ff6586c727e3a6c77.jpeg"},{"username":"inside90","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15704947/medium/c1355fcb30dd76f8e39d98d1d49f1c52.png"},{"username":"RJSkudra","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15709853/medium/c3abf2774913dc4e81fb261d36d7668c.png"},{"username":"binge203","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15713937/medium/77c1cfa4314673db80e6881fd9f64668.gif"},{"username":"Mailootje","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15714337/medium/743c3bc4ab1989966a375eeeec83d8b8.jpeg"},{"username":"tim-wiegers","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15719805/medium/08a96f064813350661cd6b20bf3d7d99.png"},{"username":"GkhnG","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15722911/medium/71a027caec489ef6ce82bcf1888329d0_default.png"},{"username":"Zoen-Millo","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15760967/medium/9e956f11adc5b34f5636268b5c485dbf.jpg"},{"username":"HooinKyoma","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15792897/medium/9489f0a9b368e0e827ae758b740a2eed.jpeg"},{"username":"wathergeen","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15794495/medium/a83b7fc80d180669ebfd6deb3544cfd0.png"},{"username":"MoeToo","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15818233/medium/d35cd6953717706eaf20f6c143c62947.png"},{"username":"itodouble","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15836233/medium/e984caea18fb0673bf319fcf28cef649.png"},{"username":"loslocitos","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15847901/medium/fe30d12fd2cf38212f929e13b169f9ec.jpeg"},{"username":"Oversleep","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15849065/medium/06141f13a6d541d753f3c2f2947b8068_default.png"},{"username":"Beardy","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15865139/medium/fca6b9d2b3f52e286d1568f52b83b6a0_default.png"},{"username":"raelyan","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15875457/medium/2f4fda1d1aaa5dcc79b328baf3f03151.jpeg"},{"username":"dolphin738","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15913763/medium/ee6fede7b8528ca642329ada80d1cc18.png"},{"username":"brunotco","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15916719/medium/09db45880fc05abc18adb8d932a5ecf9_default.png"},{"username":"LukeIsHereToDevelop","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15924739/medium/3e6cd3940297bb4ba7bb039c93e6f918.png"},{"username":"kennit","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15925879/medium/6b0733ad3c5949b91c55e4d8b03db8a5_default.png"},{"username":"kuunpire","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15950309/medium/8192a4f08f07086828ac9f74ed29a169.jpeg"},{"username":"Sandor-dev","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15951759/medium/0216c2da4eb028164ebbecf1c72f6271_default.png"},{"username":"Meierschlumpf","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15953187/medium/23c744faef1ab84fbdc9351a7850aab6.jpeg"},{"username":"harmlesscat","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15970733/medium/dd8d0214a0250c932bb518b1b55e45a2_default.png"},{"username":"OrzWTF","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15976121/medium/4c4557cbff7ff7b0503455bc59c020e0.jpeg"},{"username":"tagaishi","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15977271/medium/eade504c83a5a1ff831c80a538fbdb44_default.png"},{"username":"azurite928","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15981895/medium/547ccc459ee123e78b5401c499f1022d.png"},{"username":"Ronner231","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16021342/medium/7734d550df2de5a2fec2ffff33e7024c.jpeg"},{"username":"ugyes","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16034148/medium/ed001e3f470a2dea9a8ce955b18e7bd5.png"},{"username":"rpieja","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16045554/medium/bd55baca2ef8b92502a760cc9ee7c505_default.png"},{"username":"flambyisyou","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16051620/medium/3a3bc0c90f6b95ab4ef74396a0a17beb.png"},{"username":"Topbcy","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16077170/medium/d3aed33ea56330338756cfcd89477cfe.jpeg"},{"username":"ai5d02sb","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16084674/medium/7c8119fe2a5ca71bb15f636916a42b95_default.png"},{"username":"explosiveparrot","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16097722/medium/7762f80fc1da63f5b2eb87de9d640324_default.png"},{"username":"DestroyCom","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16111544/medium/83e3d3ac5c8bfed634cd1a074854db67.png"},{"username":"neotraxo","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16116966/medium/b30df957873329019a8a90a69f6efe3a.png"},{"username":"gzxpa","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16123314/medium/487b05577310a946b0b98bc72c11213b_default.png"},{"username":"TORIK","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16124732/medium/de770890e6411860ac06e8cc2dc0abd0_default.png"},{"username":"sergio.pibot","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16138114/medium/80db802b2fcd6bbf19ea4c7413367dca.jpeg"},{"username":"karam-ajaj","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16139838/medium/7539ab958d3c85d5f67e7849483ea8f7.jpeg"},{"username":"SimonHenz97","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16151520/medium/9b94a85b536688e2d94538c384fd0a76.png"},{"username":"olsson82","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16198988/medium/eeaab4cba210c537424d4204754b604e.png"},{"username":"bo3bdo","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16222806/medium/8652f3beacb6098dde5a4cd0a29fe80d.jpeg"},{"username":"AngryPlayer04","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16245192/medium/342715274f4dc31fd4132cdae9126188.jpeg"},{"username":"ehatamm","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16245884/medium/979d5e5526fe545be9bf46db481f7182.jpeg"},{"username":"Balionelis","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16246058/medium/986d62ba363594c517d966fe9178dcbb.png"},{"username":"kwargs","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16248816/medium/19ce11f04bf1ae2637e0483399c675f9_default.png"},{"username":"lucicu","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16273892/medium/3b09e8f3bead221ef98d6a45a16851dc_default.png"},{"username":"Michael5564445","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16357688/medium/9d9026a8bf3eaccb0417da240932ebf7.png"},{"username":"Netscream","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16359198/medium/694e0c8572dc8a47e6d63da31ec5b992.png"},{"username":"belicstefan04","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16368142/medium/4742dcfd7bf7ad7e33e37ac44fba7e0f.png"},{"username":"Hamad2066","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16371478/medium/f37bbc7685b6757defdbd1494236af61.jpeg"},{"username":"3ct0pl4sm","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16399616/medium/ce796d1c40163975624c0a483e56374f.png"},{"username":"sanchez.marcell626","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16433865/medium/3ddde1314764cc2205ffdcd1ae719a91.png"},{"username":"Minoctis","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16490079/medium/91fd0849303a4eca033d660d0a7b6854.jpeg"},{"username":"homarr","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16657003/medium/3afabfd3de949a54ff5d40ad04552aba.png"},{"username":"mwgg","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16658379/medium/763dd09f5c238cc5324d4d5357985292.png"},{"username":"kiwinho","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16671855/medium/bfe3a2d497dc4748a04d02e4ae2139cb.jpeg"},{"username":"tjvg4m34r13","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16679135/medium/7a483b9b0abe0d925e54808e1cb38fa0.jpeg"},{"username":"Diegoem","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16705921/medium/29ce68ebc191fc3f65499511b12de98f_default.png"},{"username":"bouks69","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16746091/medium/b58b1fc08520de416198be9df5d093eb.jpeg"},{"username":"futhgar","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16748067/medium/b8c526b097f9aa9d687250cac3df16dc.jpeg"},{"username":"GrimSQL","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16764343/medium/fff729a76d1f8fbcdaee9bb0c0f5f3fd.png"},{"username":"chnikel","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16767047/medium/3dda2a37e03049dafefe6697bb00f75c.png"},{"username":"yuki19870616","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16767951/medium/f59c848e38382cda6c8464b0a7b9e642_default.png"},{"username":"noamdal","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16782245/medium/b4a72a61f9caf138f0322c0cb987e6d0.png"},{"username":"MalithRukshan","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16809057/medium/084d6a5b35fbbeacc70e7021197d222f.png"},{"username":"minirope19","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16930929/medium/e8cbbc26747e31c5a8959215e1dd067b.jpeg"},{"username":"prubin","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17503492/medium/96ac87644691e49c1f983f2f094e541c_default.png"},{"username":"sam-tam-sam","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17578052/medium/1628dc2954ce3ae95e0caaa2bd60ab0c.png"},{"username":"toineenzo","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12371852/medium/9f1b2bedb4794c59479cd289bdba9fab.png"},{"username":"ArcaneWater","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12421105/medium/1ac82517a6c658f6b62c48aebcb17e86_default.png"},{"username":"Ebedami","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12974801/medium/b596dea5c09454c4c828eac558389755.jpg"},{"username":"perrygamerptyt","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13117177/medium/1406c949507ecd438480473c8a1f6124.jpg"},{"username":"SergiSvK","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13320543/medium/3df3c79cc5f0c53716c559cd3fd2fc20.png"},{"username":"jeongeon","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13362856/medium/eb572f72cde5e5526c517ca3a6489850_default.png"},{"username":"MrTranCFCVN","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13439475/medium/a1789d69c85d90c667194b5d6a518c08_default.png"},{"username":"Tobost06","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13467449/medium/9227dc07f6366bc060d28304f5cb8ce7.jpeg"},{"username":"Kf637","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13530622/medium/118b00bb72d01d3229c97409637e25d1_default.png"},{"username":"Cruellest","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13610615/medium/2a74faa0947ee08d18f7946fb8602c1e.jpg"},{"username":"olavinto","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14424644/medium/37c01732bd2b7a272b421807a05ccfeb.png"},{"username":"Thario","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14491372/medium/dd5e142070802a672900121aa3f332a6_default.png"},{"username":"Wjouter","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14585620/medium/b91a51513332bdc0a0a9ecbed99ecbc3.png"},{"username":"albanobattistella","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14686194/medium/564afcfd76f5cd24751bd669cf4943aa.jpg"},{"username":"Kachelkaiser","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14763406/medium/203a781797da4a18fe0f4e7db8fd547c.png"},{"username":"regi4","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14842936/medium/f7e1d672f562a7db61ba6b4b8f4de41d.png"},{"username":"baton5129","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14891064/medium/8d438f557b82977dd02f0ead3187265f.PNG"},{"username":"meviper","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15958903/medium/209f521ce33a05831fa8d7d1b7babc20_default.png"},{"username":"GerOriki","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16268376/medium/da00e54b08cd40348231d92873a0cd0a_default.png"},{"username":"TheRedCyclops","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16486399/medium/7f39226180120d5a9efcb8e13d8aec8e_default.png"},{"username":"dymek37","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16659221/medium/d1dc6d11d7e7fc370efc5070e837ae03.png"},{"username":"B3CKDOOR","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16744975/medium/e6c7b2d45e76465d54e6efb67c83da7d.png"},{"username":"Larsluph","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16779933/medium/ed7d36f8b44013bad3a16ac0491bd0f7.png"},{"username":"FOEDI","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16781001/medium/6b4552fb72e359aa016f7a054078fac8.png"},{"username":"xmarkurion","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16805443/medium/34a20428427de5aaef99c7d140646803.png"},{"username":"gregid","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16806705/medium/f3c8f4a20436e8538929130142bbc104.png"},{"username":"Flameberg","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16820453/medium/d47be866148474c9d05fec6fbe406d88.png"},{"username":"Playfust","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16872865/medium/5f4609cc711e5fb9fa8a0b178635769c.jpeg"},{"username":"Cotignac","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16879297/medium/81ff353edf030699a159bb79cda26e84.jpg"},{"username":"LeaHun","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16916375/medium/618520f0543b1e212ebc83d66b683d4b_default.png"},{"username":"jackwellerreal","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16919717/medium/37bbfdc03893f141133ccc619593266d.png"},{"username":"renan.costaalencar","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16938427/medium/6d25b25320c5bde734118dec4af9cd5b.png"},{"username":"andrius.cim","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16967845/medium/46028fc1a0ddfaaeebcb37b39739625d_default.png"},{"username":"xjxexrxrxyx","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17041412/medium/d34d664166db6fb25579bca9ff3b50b4.jpeg"},{"username":"bilhal.fellah","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17044572/medium/d1ec6f17e4aa013e9eededc83778d2c6.png"},{"username":"the_octo","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17058060/medium/7423ab3a4cabb67f5decb2e2dfa2b675_default.png"},{"username":"BamesJond","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17075846/medium/128d7ffd40254c8e376f45678e2fc620_default.png"},{"username":"Kornikus","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17134240/medium/d7a42b2c30db46ff152699ca01f9e11a_default.png"},{"username":"xoxyuxu","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17176526/medium/108055aa24343043de852e412bec74be.png"},{"username":"cryptodntsleep","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17249408/medium/b9e15aa2b24a0dbc1e4f194aa8ed5109.png"},{"username":"yaniorg","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17261842/medium/7dbfc77f21c826845961243c44540d26.jpg"},{"username":"Vidariondr","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17262438/medium/45fbc71be02ef27296b6f423e97b6a03_default.png"},{"username":"Fastery","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17263066/medium/701b0edc59676174863ad651528bbb37.jpeg"},{"username":"stratself","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17290388/medium/57d9d8161b3fd0abf4dc5558b4a1fa05_default.png"},{"username":"wuzeqin","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17292606/medium/b9e0308410b31337e2d7aa6ed346d718_default.png"},{"username":"ChristopherBull","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17304338/medium/21a175ce596bfae81430767467908b90.jpeg"},{"username":"darkbcnlord","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17311436/medium/b271bc0b419ec3a26ddb7b63ff8ffc4b.png"},{"username":"zhoran1997","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17344028/medium/c8f450b5a8f1e9494a45dc60af7ce63e.png"},{"username":"gilalves1703","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17447768/medium/3f1de562a5c2391c88bbf8d5cf40a266.png"},{"username":"iServeCloud","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17470960/medium/11649b99d609438d66455a4d2fdc70f7.png"},{"username":"guibolzantvbox","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17474136/medium/59a576e6da538664ceb972253916fa02.jpeg"},{"username":"ivann136","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17506062/medium/67d09ada36fac5f444a1f7517e3a97bd_default.png"},{"username":"Maheshivara","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17516082/medium/576e1e6581cf969615b265de6aeff8b7.jpeg"},{"username":"brunogui81","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17517542/medium/7c0d174e810435af518fe8e0c05e151f.jpeg"},{"username":"michelwilhelm","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17542190/medium/4c6b33fd8993ff565fa75add2ce8355e.jpeg"},{"username":"dias-gxstavo","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17547376/medium/8c6b109aae8aeb651192346a39ae9e5b.jpeg"},{"username":"ToxykAuBleu","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17614030/medium/8d21e0608560172b121bd46126d827c7.jpeg"},{"username":"e.erdemir","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17617994/medium/7259a29db9941c6171a8cbbd643b92bd.png"},{"username":"sjonkreutz","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17618306/medium/7601c45d015189a863656e21348d223b.png"},{"username":"volovich0377","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17620388/medium/c929a8fbd24f96d17620fa497ade7309.png"},{"username":"Hamstio1","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17624898/medium/406e0bf40094c24a01c3064f8f5b2a82.png"},{"username":"i3laa","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17627462/medium/613a9d283ed8b69819ee181903851660.jpeg"},{"username":"bastiengrignon","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17629884/medium/ddfc3339285bd65195fcd834e512e9b7.jpeg"},{"username":"kauhee","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17644600/medium/def1b9066cf9275b4273a5a5dca7f325.png"},{"username":"hongminzheng","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17683860/medium/a764f4963912777d27a7cd98e136a2b2.png"},{"username":"d-lindahl","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17726729/medium/cbde348582fbabcfadd89a56cd5a0b40.png"},{"username":"Enoal-Fauchille-Bolle","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17734649/medium/6e6fb29198782dc7856ea21811db669c.jpeg"}]
\ No newline at end of file
|