+
{field.key}:{" "}
{field.value}
diff --git a/apps/web/app/(app)/[emailAccountId]/debug/page.tsx b/apps/web/app/(app)/[emailAccountId]/debug/page.tsx
index 2dfba75499..22a39db1ca 100644
--- a/apps/web/app/(app)/[emailAccountId]/debug/page.tsx
+++ b/apps/web/app/(app)/[emailAccountId]/debug/page.tsx
@@ -14,6 +14,9 @@ export default async function DebugPage(props: {
Debug
+
diff --git a/apps/web/app/(app)/[emailAccountId]/debug/rules/page.tsx b/apps/web/app/(app)/[emailAccountId]/debug/rules/page.tsx
new file mode 100644
index 0000000000..cc5e58de64
--- /dev/null
+++ b/apps/web/app/(app)/[emailAccountId]/debug/rules/page.tsx
@@ -0,0 +1,96 @@
+"use client";
+
+import { useCallback, useState } from "react";
+import useSWR from "swr";
+import { useAction } from "next-safe-action/hooks";
+import { CopyIcon, CheckIcon } from "lucide-react";
+import { PageHeading } from "@/components/Typography";
+import { PageWrapper } from "@/components/PageWrapper";
+import { LoadingContent } from "@/components/LoadingContent";
+import { Button } from "@/components/ui/button";
+import { Switch } from "@/components/ui/switch";
+import { Label } from "@/components/ui/label";
+import { toastSuccess, toastError } from "@/components/Toast";
+import { toggleAllRulesAction } from "@/utils/actions/rule";
+import type { DebugRulesResponse } from "@/app/api/user/debug/rules/route";
+import { useAccount } from "@/providers/EmailAccountProvider";
+
+export default function DebugRulesPage() {
+ const { emailAccountId } = useAccount();
+ const { data, isLoading, error, mutate } = useSWR
(
+ "/api/user/debug/rules",
+ );
+ const [copied, setCopied] = useState(false);
+ const allRulesEnabled = data?.every((rule) => rule.enabled) ?? false;
+ const someRulesEnabled = data?.some((rule) => rule.enabled) ?? false;
+ const { execute, isExecuting } = useAction(
+ toggleAllRulesAction.bind(null, emailAccountId),
+ {
+ onSuccess: () => {
+ toastSuccess({ description: "Rules updated successfully" });
+ mutate();
+ },
+ onError: (result) => {
+ toastError({
+ title: "Failed to update rules",
+ description: result.error.serverError || "Unknown error",
+ });
+ },
+ },
+ );
+
+ const handleCopy = useCallback(() => {
+ if (!data) return;
+ navigator.clipboard.writeText(JSON.stringify(data, null, 2));
+ setCopied(true);
+ toastSuccess({ description: "Copied to clipboard" });
+ setTimeout(() => setCopied(false), 2000);
+ }, [data]);
+
+ return (
+
+ Rules
+
+
+
+
+
+ execute({ enabled })}
+ disabled={isExecuting}
+ />
+
+
+
+
+
+
+
+ {data ? JSON.stringify(data, null, 2) : "Loading..."}
+
+
+
+
+
+ );
+}
diff --git a/apps/web/app/api/user/debug/rules/route.ts b/apps/web/app/api/user/debug/rules/route.ts
new file mode 100644
index 0000000000..26afffd98a
--- /dev/null
+++ b/apps/web/app/api/user/debug/rules/route.ts
@@ -0,0 +1,64 @@
+import { NextResponse } from "next/server";
+import { withEmailAccount } from "@/utils/middleware";
+import prisma from "@/utils/prisma";
+
+export type DebugRulesResponse = Awaited>;
+
+export const GET = withEmailAccount("user/debug/rules", async (request) => {
+ const emailAccountId = request.auth.emailAccountId;
+ const result = await getDebugRules({ emailAccountId });
+ return NextResponse.json(result);
+});
+
+async function getDebugRules({ emailAccountId }: { emailAccountId: string }) {
+ const rules = await prisma.rule.findMany({
+ where: { emailAccountId },
+ include: {
+ actions: true,
+ group: {
+ select: {
+ id: true,
+ name: true,
+ _count: {
+ select: { items: true },
+ },
+ },
+ },
+ },
+ orderBy: { createdAt: "asc" },
+ });
+
+ return rules.map((rule) => ({
+ id: rule.id,
+ name: rule.name,
+ enabled: rule.enabled,
+ automate: rule.automate,
+ runOnThreads: rule.runOnThreads,
+ conditionalOperator: rule.conditionalOperator,
+ systemType: rule.systemType,
+ instructions: rule.instructions,
+ from: rule.from,
+ to: rule.to,
+ subject: rule.subject,
+ body: rule.body,
+ promptText: rule.promptText,
+ actions: rule.actions.map((action) => ({
+ id: action.id,
+ type: action.type,
+ label: action.label,
+ labelId: action.labelId,
+ subject: action.subject,
+ content: action.content,
+ to: action.to,
+ cc: action.cc,
+ bcc: action.bcc,
+ url: action.url,
+ folderName: action.folderName,
+ folderId: action.folderId,
+ delayInMinutes: action.delayInMinutes,
+ })),
+ learnedPatternsCount: rule.group?._count.items ?? 0,
+ createdAt: rule.createdAt,
+ updatedAt: rule.updatedAt,
+ }));
+}
diff --git a/apps/web/components/HoverCard.tsx b/apps/web/components/HoverCard.tsx
index 0081254ca9..20458a305a 100644
--- a/apps/web/components/HoverCard.tsx
+++ b/apps/web/components/HoverCard.tsx
@@ -3,6 +3,7 @@ import {
HoverCardContent,
HoverCardTrigger,
} from "@/components/ui/hover-card";
+import { cn } from "@/utils";
export function HoverCard(props: {
children: React.ReactNode;
@@ -12,7 +13,11 @@ export function HoverCard(props: {
return (
{props.children}
-
+
{props.content}
diff --git a/apps/web/utils/actions/rule.ts b/apps/web/utils/actions/rule.ts
index e15d400e10..c224bade20 100644
--- a/apps/web/utils/actions/rule.ts
+++ b/apps/web/utils/actions/rule.ts
@@ -14,6 +14,7 @@ import {
type CategoryConfig,
type CategoryAction,
toggleRuleBody,
+ toggleAllRulesBody,
} from "@/utils/actions/rule.validation";
import prisma from "@/utils/prisma";
import { isDuplicateError, isNotFoundError } from "@/utils/prisma-helpers";
@@ -443,6 +444,18 @@ export const toggleRuleAction = actionClient
},
);
+export const toggleAllRulesAction = actionClient
+ .metadata({ name: "toggleAllRules" })
+ .inputSchema(toggleAllRulesBody)
+ .action(async ({ ctx: { emailAccountId }, parsedInput: { enabled } }) => {
+ await prisma.rule.updateMany({
+ where: { emailAccountId },
+ data: { enabled },
+ });
+
+ return { success: true };
+ });
+
async function toggleRule({
ruleId,
systemType,
diff --git a/apps/web/utils/actions/rule.validation.ts b/apps/web/utils/actions/rule.validation.ts
index 0691b4bada..d88f87e24a 100644
--- a/apps/web/utils/actions/rule.validation.ts
+++ b/apps/web/utils/actions/rule.validation.ts
@@ -266,3 +266,8 @@ export const toggleRuleBody = z
.refine((data) => data.ruleId || data.systemType, {
message: "Either ruleId or systemType must be provided",
});
+
+export const toggleAllRulesBody = z.object({
+ enabled: z.boolean(),
+});
+export type ToggleAllRulesBody = z.infer;
diff --git a/version.txt b/version.txt
index fb7994a2e3..21348d8e16 100644
--- a/version.txt
+++ b/version.txt
@@ -1 +1 @@
-v2.23.5
+v2.23.6