Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/web/app/(app)/[emailAccountId]/debug/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ export default async function DebugPage(props: {
<PageHeading>Debug</PageHeading>

<div className="mt-4 flex gap-2">
<Button variant="outline" asChild>
<Link href={prefixPath(emailAccountId, "/debug/rules")}>Rules</Link>
</Button>
<Button variant="outline" asChild>
<Link href={prefixPath(emailAccountId, "/debug/drafts")}>Drafts</Link>
</Button>
Expand Down
96 changes: 96 additions & 0 deletions apps/web/app/(app)/[emailAccountId]/debug/rules/page.tsx
Original file line number Diff line number Diff line change
@@ -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<DebugRulesResponse>(

@cubic-dev-ai cubic-dev-ai Bot Dec 17, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Use useSWRWithEmailAccount instead of useSWR to ensure the fetch only happens when emailAccountId is available. This matches the pattern used in other hooks like useRules.tsx and prevents premature API calls before account context is ready.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/app/(app)/[emailAccountId]/debug/rules/page.tsx, line 20:

<comment>Use `useSWRWithEmailAccount` instead of `useSWR` to ensure the fetch only happens when `emailAccountId` is available. This matches the pattern used in other hooks like `useRules.tsx` and prevents premature API calls before account context is ready.</comment>

<file context>
@@ -0,0 +1,96 @@
+
+export default function DebugRulesPage() {
+  const { emailAccountId } = useAccount();
+  const { data, isLoading, error, mutate } = useSWR&lt;DebugRulesResponse&gt;(
+    &quot;/api/user/debug/rules&quot;,
+  );
</file context>
Fix with Cubic

"/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 (
<PageWrapper>
<PageHeading>Rules</PageHeading>

<LoadingContent loading={isLoading} error={error}>
<div className="mt-6 space-y-6">
<div className="flex items-center justify-between rounded-lg border p-4">
<div className="flex items-center gap-3">
<Switch
id="toggle-all-rules"
checked={allRulesEnabled}
onCheckedChange={(enabled) => execute({ enabled })}
disabled={isExecuting}
/>
<Label htmlFor="toggle-all-rules" className="font-medium">
{allRulesEnabled
? "All rules enabled"
: someRulesEnabled
? "Some rules enabled"
: "All rules disabled"}
</Label>
</div>
<Button
variant="outline"
size="sm"
onClick={handleCopy}
disabled={!data}
>
{copied ? (
<CheckIcon className="mr-2 h-4 w-4" />
) : (
<CopyIcon className="mr-2 h-4 w-4" />
)}
{copied ? "Copied" : "Copy JSON"}
</Button>
</div>

<div className="rounded-lg border bg-muted/50 p-4">
<pre className="overflow-auto text-sm">
{data ? JSON.stringify(data, null, 2) : "Loading..."}
</pre>
</div>
</div>
</LoadingContent>
</PageWrapper>
);
}
64 changes: 64 additions & 0 deletions apps/web/app/api/user/debug/rules/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { NextResponse } from "next/server";
import { withEmailAccount } from "@/utils/middleware";
import prisma from "@/utils/prisma";

export type DebugRulesResponse = Awaited<ReturnType<typeof getDebugRules>>;

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,
}));
}
13 changes: 13 additions & 0 deletions apps/web/utils/actions/rule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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,
Expand Down
5 changes: 5 additions & 0 deletions apps/web/utils/actions/rule.validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof toggleAllRulesBody>;
2 changes: 1 addition & 1 deletion version.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v2.23.5
v2.23.6
Loading