-
Couldn't load subscription status.
- Fork 1.1k
wip deep clean v2 #852
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
wip deep clean v2 #852
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Note Other AI code review bot(s) detectedCodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review. WalkthroughAdds a complete Deep Clean feature: frontend components and hooks for UI/progress, API endpoints and webhook workers for archive/mark-read, Redis progress tracking and Upstash/QStash publishers, AI categorization updates (priority), DB schema change (Newsletter.priority), utilities, and tests. Changes
Sequence Diagram(s)sequenceDiagram
participant UI as Browser UI
participant API as Server (actions / endpoints)
participant Queue as QStash
participant Worker as Webhook Worker
participant Email as Email Provider
participant Redis as Redis Progress Store
UI->>API: start bulk action (bulkCategory/bulkSenders/categorizeMore)
API->>Redis: createBulkOperation(emailAccountId, operationId, totalItems,...)
API->>Queue: publish job (operationId, senders/categories, webhook URL)
API-->>UI: return operationId
rect rgba(200,220,255,0.20)
loop Polling
UI->>API: GET /api/user/deep-clean/progress?operationId=...
API->>Redis: getBulkOperationProgress / getAllBulkOperations
Redis-->>API: operations...
API-->>UI: operations[]
end
end
rect rgba(220,200,255,0.20)
Queue->>Worker: deliver job (operationId, payload)
Worker->>Email: list threads per sender
Worker->>Email: process threads in batches (archive/mark-read)
Worker->>Redis: updateBulkOperationProgress(incrementCompleted/incrementFailed)
Worker->>Redis: set status "completed" when done
end
Redis-->>UI: final completed progress (via polling)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
9 issues found across 26 files
Prompt for AI agents (all 9 issues)
Understand the root cause of the following 9 issues and fix them.
<file name="apps/web/utils/actions/deep-clean.ts">
<violation number="1" location="apps/web/utils/actions/deep-clean.ts:31">
Restrict the archiveCategoryAction schema to the known category values so unexpected input can’t bypass the category filter and enqueue every sender for archiving.</violation>
<violation number="2" location="apps/web/utils/actions/deep-clean.ts:41">
"Other" category archives every sender instead of just uncategorized ones</violation>
</file>
<file name="apps/web/app/(app)/[emailAccountId]/deep-clean/BulkOperationProgress.tsx">
<violation number="1" location="apps/web/app/(app)/[emailAccountId]/deep-clean/BulkOperationProgress.tsx:35">
Avoid calling setTimeout within the render path; move this side effect into a useEffect tied to completed operations or remove it if no action is needed.</violation>
</file>
<file name="apps/web/components/DeepCleanGroupedTable.tsx">
<violation number="1" location="apps/web/components/DeepCleanGroupedTable.tsx:278">
Both Mark as Read handlers show a success toast but never invoke any mark-as-read action, so the operation has no effect even though the UI reports success. Please wire these buttons to the actual mark-as-read logic before shipping.</violation>
<violation number="2" location="apps/web/components/DeepCleanGroupedTable.tsx:629">
Passing the thread id as messageId prevents ViewEmailButton from opening the selected message. Use the actual message id from the thread instead.</violation>
</file>
<file name="apps/web/utils/redis/bulk-operation-progress.ts">
<violation number="1" location="apps/web/utils/redis/bulk-operation-progress.ts:108">
The auto-complete block overwrites an explicit failure status, causing failed operations to be reported as completed.</violation>
<violation number="2" location="apps/web/utils/redis/bulk-operation-progress.ts:122">
Using redis.keys() to enumerate keys is blocking and can stall Redis under load; switch to a SCAN-based iterator instead.</violation>
</file>
<file name="apps/web/utils/email.test.ts">
<violation number="1" location="apps/web/utils/email.test.ts:91">
This expectation keeps surrounding quotes, but extractNameFromEmail removes all leading/trailing quote characters, so the test will fail—expect 'quoted name' instead.</violation>
<violation number="2" location="apps/web/utils/email.test.ts:94">
This test expects extractNameFromEmail to keep outer single quotes, but the function removes them, so adjust the expectation to 'quoted name' to match real behavior.</violation>
</file>
React with 👍 or 👎 to teach cubic. Mention @cubic-dev-ai to give feedback, ask questions, or re-run the review.
apps/web/utils/actions/deep-clean.ts
Outdated
| .metadata({ name: "archiveCategory" }) | ||
| .schema( | ||
| z.object({ | ||
| category: z.string(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Restrict the archiveCategoryAction schema to the known category values so unexpected input can’t bypass the category filter and enqueue every sender for archiving.
Prompt for AI agents
Address the following comment on apps/web/utils/actions/deep-clean.ts at line 31:
<comment>Restrict the archiveCategoryAction schema to the known category values so unexpected input can’t bypass the category filter and enqueue every sender for archiving.</comment>
<file context>
@@ -0,0 +1,310 @@
+ .metadata({ name: "archiveCategory" })
+ .schema(
+ z.object({
+ category: z.string(),
+ }),
+ )
</file context>
|
|
||
| // Hide completed operations after 5 seconds | ||
| if (isCompleted) { | ||
| setTimeout(() => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Avoid calling setTimeout within the render path; move this side effect into a useEffect tied to completed operations or remove it if no action is needed.
Prompt for AI agents
Address the following comment on apps/web/app/(app)/[emailAccountId]/deep-clean/BulkOperationProgress.tsx at line 35:
<comment>Avoid calling setTimeout within the render path; move this side effect into a useEffect tied to completed operations or remove it if no action is needed.</comment>
<file context>
@@ -0,0 +1,63 @@
+
+ // Hide completed operations after 5 seconds
+ if (isCompleted) {
+ setTimeout(() => {
+ // This will be handled by React's re-render when the operation is removed from Redis
+ }, 5000);
</file context>
| // Mark selected as read | ||
| const markSelectedAsRead = async () => { | ||
| // TODO: Implement mark as read functionality | ||
| toastSuccess({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Both Mark as Read handlers show a success toast but never invoke any mark-as-read action, so the operation has no effect even though the UI reports success. Please wire these buttons to the actual mark-as-read logic before shipping.
Prompt for AI agents
Address the following comment on apps/web/components/DeepCleanGroupedTable.tsx at line 278:
<comment>Both Mark as Read handlers show a success toast but never invoke any mark-as-read action, so the operation has no effect even though the UI reports success. Please wire these buttons to the actual mark-as-read logic before shipping.</comment>
<file context>
@@ -0,0 +1,679 @@
+ // Mark selected as read
+ const markSelectedAsRead = async () => {
+ // TODO: Implement mark as read functionality
+ toastSuccess({
+ description: `Marked ${selectedSenders.size} senders as read`,
+ });
</file context>
✅ Addressed in 9f182e9
| return ( | ||
| <TableRow key={thread.id} className="bg-muted/50"> | ||
| <TableCell className="py-3"> | ||
| <ViewEmailButton threadId={thread.id} messageId={thread.id} /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Passing the thread id as messageId prevents ViewEmailButton from opening the selected message. Use the actual message id from the thread instead.
Prompt for AI agents
Address the following comment on apps/web/components/DeepCleanGroupedTable.tsx at line 629:
<comment>Passing the thread id as messageId prevents ViewEmailButton from opening the selected message. Use the actual message id from the thread instead.</comment>
<file context>
@@ -0,0 +1,679 @@
+ return (
+ <TableRow key={thread.id} className="bg-muted/50">
+ <TableCell className="py-3">
+ <ViewEmailButton threadId={thread.id} messageId={thread.id} />
+ </TableCell>
+ <TableCell className="py-3">
</file context>
| <ViewEmailButton threadId={thread.id} messageId={thread.id} /> | |
| <ViewEmailButton threadId={thread.id} messageId={firstMessage.id} /> |
| emailAccountId: string; | ||
| }): Promise<Array<RedisBulkOperationProgress & { operationId: string }>> { | ||
| const pattern = `bulk-operation-progress:${emailAccountId}:*`; | ||
| const keys = await redis.keys(pattern); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using redis.keys() to enumerate keys is blocking and can stall Redis under load; switch to a SCAN-based iterator instead.
Prompt for AI agents
Address the following comment on apps/web/utils/redis/bulk-operation-progress.ts at line 122:
<comment>Using redis.keys() to enumerate keys is blocking and can stall Redis under load; switch to a SCAN-based iterator instead.</comment>
<file context>
@@ -0,0 +1,139 @@
+ emailAccountId: string;
+}): Promise<Array<RedisBulkOperationProgress & { operationId: string }>> {
+ const pattern = `bulk-operation-progress:${emailAccountId}:*`;
+ const keys = await redis.keys(pattern);
+
+ if (keys.length === 0) return [];
</file context>
| updatedProgress.completedItems + updatedProgress.failedItems >= | ||
| updatedProgress.totalItems | ||
| ) { | ||
| updatedProgress.status = "completed"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The auto-complete block overwrites an explicit failure status, causing failed operations to be reported as completed.
Prompt for AI agents
Address the following comment on apps/web/utils/redis/bulk-operation-progress.ts at line 108:
<comment>The auto-complete block overwrites an explicit failure status, causing failed operations to be reported as completed.</comment>
<file context>
@@ -0,0 +1,139 @@
+ updatedProgress.completedItems + updatedProgress.failedItems >=
+ updatedProgress.totalItems
+ ) {
+ updatedProgress.status = "completed";
+ }
+
</file context>
| updatedProgress.status = "completed"; | |
| updatedProgress.status = updatedProgress.status === "failed" ? "failed" : "completed"; |
| '"quoted name"', | ||
| ); | ||
| expect(extractNameFromEmail("''quoted name'' <[email protected]>")).toBe( | ||
| "'quoted name'", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test expects extractNameFromEmail to keep outer single quotes, but the function removes them, so adjust the expectation to 'quoted name' to match real behavior.
Prompt for AI agents
Address the following comment on apps/web/utils/email.test.ts at line 94:
<comment>This test expects extractNameFromEmail to keep outer single quotes, but the function removes them, so adjust the expectation to 'quoted name' to match real behavior.</comment>
<file context>
@@ -30,6 +30,70 @@ describe("email utils", () => {
+ '"quoted name"',
+ );
+ expect(extractNameFromEmail("''quoted name'' <[email protected]>")).toBe(
+ "'quoted name'",
+ );
+ });
</file context>
| "'quoted name'", | |
| "quoted name", |
|
|
||
| it("handles multiple quotes correctly", () => { | ||
| expect(extractNameFromEmail('""quoted name"" <[email protected]>')).toBe( | ||
| '"quoted name"', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This expectation keeps surrounding quotes, but extractNameFromEmail removes all leading/trailing quote characters, so the test will fail—expect 'quoted name' instead.
Prompt for AI agents
Address the following comment on apps/web/utils/email.test.ts at line 91:
<comment>This expectation keeps surrounding quotes, but extractNameFromEmail removes all leading/trailing quote characters, so the test will fail—expect 'quoted name' instead.</comment>
<file context>
@@ -30,6 +30,70 @@ describe("email utils", () => {
+
+ it("handles multiple quotes correctly", () => {
+ expect(extractNameFromEmail('""quoted name"" <[email protected]>')).toBe(
+ '"quoted name"',
+ );
+ expect(extractNameFromEmail("''quoted name'' <[email protected]>")).toBe(
</file context>
| '"quoted name"', | |
| "quoted name", |
| logger.info("Archiving category", { category, emailAccountId }); | ||
|
|
||
| // Get all senders in this category | ||
| const senders = await prisma.newsletter.findMany({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"Other" category archives every sender instead of just uncategorized ones
Prompt for AI agents
Address the following comment on apps/web/utils/actions/deep-clean.ts at line 41:
<comment>"Other" category archives every sender instead of just uncategorized ones</comment>
<file context>
@@ -0,0 +1,310 @@
+ logger.info("Archiving category", { category, emailAccountId });
+
+ // Get all senders in this category
+ const senders = await prisma.newsletter.findMany({
+ where: {
+ emailAccountId,
</file context>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 11
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/web/utils/categorize/senders/categorize.ts (1)
22-43: Restore default categories when user data is missingWith the new default-category flow,
categorizeSendernow falls back to an empty list whenuserCategoriesis absent. That means we hand zero categories toaiCategorizeSender, so it cannot return a category or priority—categorization silently fails and we always returnundefined. Instead, we must load the default categories in this case.- const categories = userCategories || []; + const categories = + userCategories ?? (await getCategories()).categories;
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (26)
apps/web/app/(app)/[emailAccountId]/deep-clean/BulkOperationProgress.tsx(1 hunks)apps/web/app/(app)/[emailAccountId]/deep-clean/CategorizeProgress.tsx(1 hunks)apps/web/app/(app)/[emailAccountId]/deep-clean/CategorizeWithAiButton.tsx(1 hunks)apps/web/app/(app)/[emailAccountId]/deep-clean/CreateCategoryButton.tsx(1 hunks)apps/web/app/(app)/[emailAccountId]/deep-clean/DeepCleanContent.tsx(1 hunks)apps/web/app/(app)/[emailAccountId]/deep-clean/Uncategorized.tsx(1 hunks)apps/web/app/(app)/[emailAccountId]/deep-clean/page.tsx(1 hunks)apps/web/app/api/user/categorize/senders/batch/handle-batch.ts(2 hunks)apps/web/app/api/user/deep-clean/progress/route.ts(1 hunks)apps/web/app/api/user/deep-clean/senders/route.ts(1 hunks)apps/web/app/api/webhooks/deep-clean/archive/route.ts(1 hunks)apps/web/app/api/webhooks/deep-clean/mark-read/route.ts(1 hunks)apps/web/components/DeepCleanGroupedTable.tsx(1 hunks)apps/web/components/SideNav.tsx(1 hunks)apps/web/hooks/useDeepClean.ts(1 hunks)apps/web/prisma/schema.prisma(2 hunks)apps/web/utils/actions/deep-clean.ts(1 hunks)apps/web/utils/ai/categorize-sender/ai-categorize-senders.ts(5 hunks)apps/web/utils/ai/categorize-sender/ai-categorize-single-sender.ts(2 hunks)apps/web/utils/categorize/senders/categorize.ts(5 hunks)apps/web/utils/email.test.ts(1 hunks)apps/web/utils/email.ts(1 hunks)apps/web/utils/priority.ts(1 hunks)apps/web/utils/redis/bulk-operation-progress.ts(1 hunks)apps/web/utils/upstash/bulk-operations.ts(1 hunks)apps/web/utils/upstash/deep-clean-categorization.ts(1 hunks)
🧰 Additional context used
📓 Path-based instructions (33)
apps/web/**/*.{ts,tsx}
📄 CodeRabbit inference engine (apps/web/CLAUDE.md)
apps/web/**/*.{ts,tsx}: Use TypeScript with strict null checks
Path aliases: Use@/for imports from project root
Use proper error handling with try/catch blocks
Format code with Prettier
Leverage TypeScript inference for better DX
Files:
apps/web/app/(app)/[emailAccountId]/deep-clean/CategorizeProgress.tsxapps/web/utils/ai/categorize-sender/ai-categorize-senders.tsapps/web/app/(app)/[emailAccountId]/deep-clean/DeepCleanContent.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/CategorizeWithAiButton.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/page.tsxapps/web/utils/email.test.tsapps/web/app/api/user/categorize/senders/batch/handle-batch.tsapps/web/utils/actions/deep-clean.tsapps/web/app/(app)/[emailAccountId]/deep-clean/BulkOperationProgress.tsxapps/web/utils/categorize/senders/categorize.tsapps/web/utils/priority.tsapps/web/app/api/user/deep-clean/progress/route.tsapps/web/app/api/webhooks/deep-clean/mark-read/route.tsapps/web/utils/redis/bulk-operation-progress.tsapps/web/utils/upstash/deep-clean-categorization.tsapps/web/utils/email.tsapps/web/app/(app)/[emailAccountId]/deep-clean/CreateCategoryButton.tsxapps/web/utils/upstash/bulk-operations.tsapps/web/hooks/useDeepClean.tsapps/web/components/SideNav.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/Uncategorized.tsxapps/web/app/api/webhooks/deep-clean/archive/route.tsapps/web/app/api/user/deep-clean/senders/route.tsapps/web/components/DeepCleanGroupedTable.tsxapps/web/utils/ai/categorize-sender/ai-categorize-single-sender.ts
apps/web/app/**
📄 CodeRabbit inference engine (apps/web/CLAUDE.md)
NextJS app router structure with (app) directory
Files:
apps/web/app/(app)/[emailAccountId]/deep-clean/CategorizeProgress.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/DeepCleanContent.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/CategorizeWithAiButton.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/page.tsxapps/web/app/api/user/categorize/senders/batch/handle-batch.tsapps/web/app/(app)/[emailAccountId]/deep-clean/BulkOperationProgress.tsxapps/web/app/api/user/deep-clean/progress/route.tsapps/web/app/api/webhooks/deep-clean/mark-read/route.tsapps/web/app/(app)/[emailAccountId]/deep-clean/CreateCategoryButton.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/Uncategorized.tsxapps/web/app/api/webhooks/deep-clean/archive/route.tsapps/web/app/api/user/deep-clean/senders/route.ts
apps/web/**/*.tsx
📄 CodeRabbit inference engine (apps/web/CLAUDE.md)
apps/web/**/*.tsx: Follow tailwindcss patterns with prettier-plugin-tailwindcss
Prefer functional components with hooks
Use shadcn/ui components when available
Ensure responsive design with mobile-first approach
Follow consistent naming conventions (PascalCase for components)
Use LoadingContent component for async data
Useresult?.serverErrorwithtoastErrorandtoastSuccess
UseLoadingContentcomponent to handle loading and error states consistently
Passloading,error, and children props toLoadingContent
Files:
apps/web/app/(app)/[emailAccountId]/deep-clean/CategorizeProgress.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/DeepCleanContent.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/CategorizeWithAiButton.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/page.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/BulkOperationProgress.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/CreateCategoryButton.tsxapps/web/components/SideNav.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/Uncategorized.tsxapps/web/components/DeepCleanGroupedTable.tsx
!{.cursor/rules/*.mdc}
📄 CodeRabbit inference engine (.cursor/rules/cursor-rules.mdc)
Never place rule files in the project root, in subdirectories outside .cursor/rules, or in any other location
Files:
apps/web/app/(app)/[emailAccountId]/deep-clean/CategorizeProgress.tsxapps/web/utils/ai/categorize-sender/ai-categorize-senders.tsapps/web/app/(app)/[emailAccountId]/deep-clean/DeepCleanContent.tsxapps/web/prisma/schema.prismaapps/web/app/(app)/[emailAccountId]/deep-clean/CategorizeWithAiButton.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/page.tsxapps/web/utils/email.test.tsapps/web/app/api/user/categorize/senders/batch/handle-batch.tsapps/web/utils/actions/deep-clean.tsapps/web/app/(app)/[emailAccountId]/deep-clean/BulkOperationProgress.tsxapps/web/utils/categorize/senders/categorize.tsapps/web/utils/priority.tsapps/web/app/api/user/deep-clean/progress/route.tsapps/web/app/api/webhooks/deep-clean/mark-read/route.tsapps/web/utils/redis/bulk-operation-progress.tsapps/web/utils/upstash/deep-clean-categorization.tsapps/web/utils/email.tsapps/web/app/(app)/[emailAccountId]/deep-clean/CreateCategoryButton.tsxapps/web/utils/upstash/bulk-operations.tsapps/web/hooks/useDeepClean.tsapps/web/components/SideNav.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/Uncategorized.tsxapps/web/app/api/webhooks/deep-clean/archive/route.tsapps/web/app/api/user/deep-clean/senders/route.tsapps/web/components/DeepCleanGroupedTable.tsxapps/web/utils/ai/categorize-sender/ai-categorize-single-sender.ts
**/*.tsx
📄 CodeRabbit inference engine (.cursor/rules/form-handling.mdc)
**/*.tsx: Use React Hook Form with Zod for validation
Validate form inputs before submission
Show validation errors inline next to form fields
Files:
apps/web/app/(app)/[emailAccountId]/deep-clean/CategorizeProgress.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/DeepCleanContent.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/CategorizeWithAiButton.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/page.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/BulkOperationProgress.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/CreateCategoryButton.tsxapps/web/components/SideNav.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/Uncategorized.tsxapps/web/components/DeepCleanGroupedTable.tsx
**/*.{ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/logging.mdc)
**/*.{ts,tsx}: UsecreateScopedLoggerfor logging in backend TypeScript files
Typically add the logger initialization at the top of the file when usingcreateScopedLogger
Only use.with()on a logger instance within a specific function, not for a global loggerImport Prisma in the project using
import prisma from "@/utils/prisma";
**/*.{ts,tsx}: Don't use TypeScript enums.
Don't use TypeScript const enum.
Don't use the TypeScript directive @ts-ignore.
Don't use primitive type aliases or misleading types.
Don't use empty type parameters in type aliases and interfaces.
Don't use any or unknown as type constraints.
Don't use implicit any type on variable declarations.
Don't let variables evolve into any type through reassignments.
Don't use non-null assertions with the ! postfix operator.
Don't misuse the non-null assertion operator (!) in TypeScript files.
Don't use user-defined types.
Use as const instead of literal types and type annotations.
Use export type for types.
Use import type for types.
Don't declare empty interfaces.
Don't merge interfaces and classes unsafely.
Don't use overload signatures that aren't next to each other.
Use the namespace keyword instead of the module keyword to declare TypeScript namespaces.
Don't use TypeScript namespaces.
Don't export imported variables.
Don't add type annotations to variables, parameters, and class properties that are initialized with literal expressions.
Don't use parameter properties in class constructors.
Use either T[] or Array consistently.
Initialize each enum member value explicitly.
Make sure all enum members are literal values.
Files:
apps/web/app/(app)/[emailAccountId]/deep-clean/CategorizeProgress.tsxapps/web/utils/ai/categorize-sender/ai-categorize-senders.tsapps/web/app/(app)/[emailAccountId]/deep-clean/DeepCleanContent.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/CategorizeWithAiButton.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/page.tsxapps/web/utils/email.test.tsapps/web/app/api/user/categorize/senders/batch/handle-batch.tsapps/web/utils/actions/deep-clean.tsapps/web/app/(app)/[emailAccountId]/deep-clean/BulkOperationProgress.tsxapps/web/utils/categorize/senders/categorize.tsapps/web/utils/priority.tsapps/web/app/api/user/deep-clean/progress/route.tsapps/web/app/api/webhooks/deep-clean/mark-read/route.tsapps/web/utils/redis/bulk-operation-progress.tsapps/web/utils/upstash/deep-clean-categorization.tsapps/web/utils/email.tsapps/web/app/(app)/[emailAccountId]/deep-clean/CreateCategoryButton.tsxapps/web/utils/upstash/bulk-operations.tsapps/web/hooks/useDeepClean.tsapps/web/components/SideNav.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/Uncategorized.tsxapps/web/app/api/webhooks/deep-clean/archive/route.tsapps/web/app/api/user/deep-clean/senders/route.tsapps/web/components/DeepCleanGroupedTable.tsxapps/web/utils/ai/categorize-sender/ai-categorize-single-sender.ts
apps/web/app/(app)/*/**
📄 CodeRabbit inference engine (.cursor/rules/page-structure.mdc)
Components for the page are either put in page.tsx, or in the apps/web/app/(app)/PAGE_NAME folder
Files:
apps/web/app/(app)/[emailAccountId]/deep-clean/CategorizeProgress.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/DeepCleanContent.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/CategorizeWithAiButton.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/page.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/BulkOperationProgress.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/CreateCategoryButton.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/Uncategorized.tsx
apps/web/app/(app)/*/**/*.tsx
📄 CodeRabbit inference engine (.cursor/rules/page-structure.mdc)
If you need to use onClick in a component, that component is a client component and file must start with 'use client'
Files:
apps/web/app/(app)/[emailAccountId]/deep-clean/CategorizeProgress.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/DeepCleanContent.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/CategorizeWithAiButton.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/page.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/BulkOperationProgress.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/CreateCategoryButton.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/Uncategorized.tsx
apps/web/app/(app)/*/**/**/*.tsx
📄 CodeRabbit inference engine (.cursor/rules/page-structure.mdc)
If we're in a deeply nested component we will use swr to fetch via API
Files:
apps/web/app/(app)/[emailAccountId]/deep-clean/CategorizeProgress.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/DeepCleanContent.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/CategorizeWithAiButton.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/page.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/BulkOperationProgress.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/CreateCategoryButton.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/Uncategorized.tsx
apps/web/app/**/*.tsx
📄 CodeRabbit inference engine (.cursor/rules/project-structure.mdc)
Components with
onClickmust be client components withuse clientdirective
Files:
apps/web/app/(app)/[emailAccountId]/deep-clean/CategorizeProgress.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/DeepCleanContent.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/CategorizeWithAiButton.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/page.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/BulkOperationProgress.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/CreateCategoryButton.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/Uncategorized.tsx
**/*.{js,jsx,ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/ultracite.mdc)
**/*.{js,jsx,ts,tsx}: Don't useelements in Next.js projects.
Don't use elements in Next.js projects.
Don't use namespace imports.
Don't access namespace imports dynamically.
Don't use global eval().
Don't use console.
Don't use debugger.
Don't use var.
Don't use with statements in non-strict contexts.
Don't use the arguments object.
Don't use consecutive spaces in regular expression literals.
Don't use the comma operator.
Don't use unnecessary boolean casts.
Don't use unnecessary callbacks with flatMap.
Use for...of statements instead of Array.forEach.
Don't create classes that only have static members (like a static namespace).
Don't use this and super in static contexts.
Don't use unnecessary catch clauses.
Don't use unnecessary constructors.
Don't use unnecessary continue statements.
Don't export empty modules that don't change anything.
Don't use unnecessary escape sequences in regular expression literals.
Don't use unnecessary labels.
Don't use unnecessary nested block statements.
Don't rename imports, exports, and destructured assignments to the same name.
Don't use unnecessary string or template literal concatenation.
Don't use String.raw in template literals when there are no escape sequences.
Don't use useless case statements in switch statements.
Don't use ternary operators when simpler alternatives exist.
Don't use useless this aliasing.
Don't initialize variables to undefined.
Don't use the void operators (they're not familiar).
Use arrow functions instead of function expressions.
Use Date.now() to get milliseconds since the Unix Epoch.
Use .flatMap() instead of map().flat() when possible.
Use literal property access instead of computed property access.
Don't use parseInt() or Number.parseInt() when binary, octal, or hexadecimal literals work.
Use concise optional chaining instead of chained logical expressions.
Use regular expression literals instead of the RegExp constructor when possible.
Don't use number literal object member names th...
Files:
apps/web/app/(app)/[emailAccountId]/deep-clean/CategorizeProgress.tsxapps/web/utils/ai/categorize-sender/ai-categorize-senders.tsapps/web/app/(app)/[emailAccountId]/deep-clean/DeepCleanContent.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/CategorizeWithAiButton.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/page.tsxapps/web/utils/email.test.tsapps/web/app/api/user/categorize/senders/batch/handle-batch.tsapps/web/utils/actions/deep-clean.tsapps/web/app/(app)/[emailAccountId]/deep-clean/BulkOperationProgress.tsxapps/web/utils/categorize/senders/categorize.tsapps/web/utils/priority.tsapps/web/app/api/user/deep-clean/progress/route.tsapps/web/app/api/webhooks/deep-clean/mark-read/route.tsapps/web/utils/redis/bulk-operation-progress.tsapps/web/utils/upstash/deep-clean-categorization.tsapps/web/utils/email.tsapps/web/app/(app)/[emailAccountId]/deep-clean/CreateCategoryButton.tsxapps/web/utils/upstash/bulk-operations.tsapps/web/hooks/useDeepClean.tsapps/web/components/SideNav.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/Uncategorized.tsxapps/web/app/api/webhooks/deep-clean/archive/route.tsapps/web/app/api/user/deep-clean/senders/route.tsapps/web/components/DeepCleanGroupedTable.tsxapps/web/utils/ai/categorize-sender/ai-categorize-single-sender.ts
!pages/_document.{js,jsx,ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/ultracite.mdc)
!pages/_document.{js,jsx,ts,tsx}: Don't import next/document outside of pages/_document.jsx in Next.js projects.
Don't import next/document outside of pages/_document.jsx in Next.js projects.
Files:
apps/web/app/(app)/[emailAccountId]/deep-clean/CategorizeProgress.tsxapps/web/utils/ai/categorize-sender/ai-categorize-senders.tsapps/web/app/(app)/[emailAccountId]/deep-clean/DeepCleanContent.tsxapps/web/prisma/schema.prismaapps/web/app/(app)/[emailAccountId]/deep-clean/CategorizeWithAiButton.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/page.tsxapps/web/utils/email.test.tsapps/web/app/api/user/categorize/senders/batch/handle-batch.tsapps/web/utils/actions/deep-clean.tsapps/web/app/(app)/[emailAccountId]/deep-clean/BulkOperationProgress.tsxapps/web/utils/categorize/senders/categorize.tsapps/web/utils/priority.tsapps/web/app/api/user/deep-clean/progress/route.tsapps/web/app/api/webhooks/deep-clean/mark-read/route.tsapps/web/utils/redis/bulk-operation-progress.tsapps/web/utils/upstash/deep-clean-categorization.tsapps/web/utils/email.tsapps/web/app/(app)/[emailAccountId]/deep-clean/CreateCategoryButton.tsxapps/web/utils/upstash/bulk-operations.tsapps/web/hooks/useDeepClean.tsapps/web/components/SideNav.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/Uncategorized.tsxapps/web/app/api/webhooks/deep-clean/archive/route.tsapps/web/app/api/user/deep-clean/senders/route.tsapps/web/components/DeepCleanGroupedTable.tsxapps/web/utils/ai/categorize-sender/ai-categorize-single-sender.ts
**/*.{jsx,tsx}
📄 CodeRabbit inference engine (.cursor/rules/ultracite.mdc)
**/*.{jsx,tsx}: Don't destructure props inside JSX components in Solid projects.
Don't use both children and dangerouslySetInnerHTML props on the same element.
Don't use Array index in keys.
Don't assign to React component props.
Don't define React components inside other components.
Don't use event handlers on non-interactive elements.
Don't assign JSX properties multiple times.
Don't add extra closing tags for components without children.
Use <>...</> instead of ....
Don't insert comments as text nodes.
Don't use the return value of React.render.
Make sure all dependencies are correctly specified in React hooks.
Make sure all React hooks are called from the top level of component functions.
Don't use unnecessary fragments.
Don't pass children as props.
Use semantic elements instead of role attributes in JSX.
Files:
apps/web/app/(app)/[emailAccountId]/deep-clean/CategorizeProgress.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/DeepCleanContent.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/CategorizeWithAiButton.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/page.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/BulkOperationProgress.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/CreateCategoryButton.tsxapps/web/components/SideNav.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/Uncategorized.tsxapps/web/components/DeepCleanGroupedTable.tsx
**/*.{html,jsx,tsx}
📄 CodeRabbit inference engine (.cursor/rules/ultracite.mdc)
**/*.{html,jsx,tsx}: Don't use or elements.
Don't use accessKey attribute on any HTML element.
Don't set aria-hidden="true" on focusable elements.
Don't add ARIA roles, states, and properties to elements that don't support them.
Only use the scope prop on elements.
Don't assign non-interactive ARIA roles to interactive HTML elements.
Make sure label elements have text content and are associated with an input.
Don't assign interactive ARIA roles to non-interactive HTML elements.
Don't assign tabIndex to non-interactive HTML elements.
Don't use positive integers for tabIndex property.
Don't include "image", "picture", or "photo" in img alt prop.
Don't use explicit role property that's the same as the implicit/default role.
Make static elements with click handlers use a valid role attribute.
Always include a title element for SVG elements.
Give all elements requiring alt text meaningful information for screen readers.
Make sure anchors have content that's accessible to screen readers.
Assign tabIndex to non-interactive HTML elements with aria-activedescendant.
Include all required ARIA attributes for elements with ARIA roles.
Make sure ARIA properties are valid for the element's supported roles.
Always include a type attribute for button elements.
Make elements with interactive roles and handlers focusable.
Give heading elements content that's accessible to screen readers (not hidden with aria-hidden).
Always include a lang attribute on the html element.
Always include a title attribute for iframe elements.
Accompany onClick with at least one of: onKeyUp, onKeyDown, or onKeyPress.
Accompany onMouseOver/onMouseOut with onFocus/onBlur.
Include caption tracks for audio and video elements.
Make sure all anchors are valid and navigable.
Ensure all ARIA properties (aria-*) are valid.
Use valid, non-abstract ARIA roles for elements with ARIA roles.
Use valid ARIA state and property values.
Use valid values for the autocomplete attribute on input eleme...Files:
apps/web/app/(app)/[emailAccountId]/deep-clean/CategorizeProgress.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/DeepCleanContent.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/CategorizeWithAiButton.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/page.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/BulkOperationProgress.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/CreateCategoryButton.tsxapps/web/components/SideNav.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/Uncategorized.tsxapps/web/components/DeepCleanGroupedTable.tsx**/*.ts
📄 CodeRabbit inference engine (.cursor/rules/form-handling.mdc)
**/*.ts: The same validation should be done in the server action too
Define validation schemas using ZodFiles:
apps/web/utils/ai/categorize-sender/ai-categorize-senders.tsapps/web/utils/email.test.tsapps/web/app/api/user/categorize/senders/batch/handle-batch.tsapps/web/utils/actions/deep-clean.tsapps/web/utils/categorize/senders/categorize.tsapps/web/utils/priority.tsapps/web/app/api/user/deep-clean/progress/route.tsapps/web/app/api/webhooks/deep-clean/mark-read/route.tsapps/web/utils/redis/bulk-operation-progress.tsapps/web/utils/upstash/deep-clean-categorization.tsapps/web/utils/email.tsapps/web/utils/upstash/bulk-operations.tsapps/web/hooks/useDeepClean.tsapps/web/app/api/webhooks/deep-clean/archive/route.tsapps/web/app/api/user/deep-clean/senders/route.tsapps/web/utils/ai/categorize-sender/ai-categorize-single-sender.tsapps/web/utils/**
📄 CodeRabbit inference engine (.cursor/rules/project-structure.mdc)
Create utility functions in
utils/folder for reusable logicFiles:
apps/web/utils/ai/categorize-sender/ai-categorize-senders.tsapps/web/utils/email.test.tsapps/web/utils/actions/deep-clean.tsapps/web/utils/categorize/senders/categorize.tsapps/web/utils/priority.tsapps/web/utils/redis/bulk-operation-progress.tsapps/web/utils/upstash/deep-clean-categorization.tsapps/web/utils/email.tsapps/web/utils/upstash/bulk-operations.tsapps/web/utils/ai/categorize-sender/ai-categorize-single-sender.tsapps/web/utils/**/*.ts
📄 CodeRabbit inference engine (.cursor/rules/project-structure.mdc)
apps/web/utils/**/*.ts: Use lodash utilities for common operations (arrays, objects, strings)
Import specific lodash functions to minimize bundle sizeFiles:
apps/web/utils/ai/categorize-sender/ai-categorize-senders.tsapps/web/utils/email.test.tsapps/web/utils/actions/deep-clean.tsapps/web/utils/categorize/senders/categorize.tsapps/web/utils/priority.tsapps/web/utils/redis/bulk-operation-progress.tsapps/web/utils/upstash/deep-clean-categorization.tsapps/web/utils/email.tsapps/web/utils/upstash/bulk-operations.tsapps/web/utils/ai/categorize-sender/ai-categorize-single-sender.tsapps/web/utils/ai/**/*.{ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/llm.mdc)
apps/web/utils/ai/**/*.{ts,tsx}: Place main LLM feature implementations under apps/web/utils/ai/
LLM feature functions should follow the provided TypeScript pattern (separate system/user prompts, use createGenerateObject, Zod schema validation, early validation, return result.object)
Keep system prompts and user prompts separate
System prompt should define the LLM's role and task specifications
User prompt should contain the actual data and context
Always define a Zod schema for response validation
Make Zod schemas as specific as possible to guide LLM output
Use descriptive scoped loggers for each feature
Log inputs and outputs with appropriate log levels and include relevant context
Implement early returns for invalid inputs
Use proper error types and logging for failures
Implement fallbacks for AI failures
Add retry logic for transient failures using withRetry
Use XML-like tags to structure data in prompts
Remove excessive whitespace and truncate long inputs in prompts
Format prompt data consistently across similar functions
Use TypeScript types for all parameters and return values in LLM features
Define clear interfaces for complex input/output structures in LLM featuresFiles:
apps/web/utils/ai/categorize-sender/ai-categorize-senders.tsapps/web/utils/ai/categorize-sender/ai-categorize-single-sender.tsapps/web/utils/{ai,llms}/**/*.{ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/llm.mdc)
Keep related AI functions co-located and extract common patterns into utilities; document complex AI logic with clear comments
Files:
apps/web/utils/ai/categorize-sender/ai-categorize-senders.tsapps/web/utils/ai/categorize-sender/ai-categorize-single-sender.tsapps/web/prisma/schema.prisma
📄 CodeRabbit inference engine (.cursor/rules/prisma.mdc)
The Prisma schema file must be located at
apps/web/prisma/schema.prismaFiles:
apps/web/prisma/schema.prisma**/*.test.{ts,js}
📄 CodeRabbit inference engine (.cursor/rules/security.mdc)
Include security tests in your test suites to verify authentication, authorization, and error handling.
Files:
apps/web/utils/email.test.ts**/*.{test,spec}.{js,jsx,ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/ultracite.mdc)
**/*.{test,spec}.{js,jsx,ts,tsx}: Don't use export or module.exports in test files.
Don't use focused tests.
Don't use disabled tests.
Make sure the assertion function, like expect, is placed inside an it() function call.
Don't nest describe() blocks too deeply in test files.
Don't use focused tests.
Don't use disabled tests.
Don't use export or module.exports in test files.Files:
apps/web/utils/email.test.ts**/*.test.{ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/testing.mdc)
**/*.test.{ts,tsx}: Use Vitest (vitest) as the testing framework
Colocate tests next to the file under test (e.g., dir/format.ts with dir/format.test.ts)
In tests, mock theserver-onlymodule withvi.mock("server-only", () => ({}));
When testing code that uses Prisma, mock it withvi.mock("@/utils/prisma")and use the mock from@/utils/__mocks__/prisma
Use provided helpers for mocks: import{ getEmail, getEmailAccount, getRule }from@/__tests__/helpers
Each test should be independent
Use descriptive test names
Mock external dependencies in tests
Clean up mocks between tests (e.g.,vi.clearAllMocks()inbeforeEach)
Avoid testing implementation details; focus on observable behavior
Do not mock the LoggerFiles:
apps/web/utils/email.test.tsapps/web/app/api/**/*.{ts,js}
📄 CodeRabbit inference engine (.cursor/rules/security-audit.mdc)
apps/web/app/api/**/*.{ts,js}: All API route handlers in 'apps/web/app/api/' must use authentication middleware: withAuth, withEmailAccount, or withError (with custom authentication logic).
All Prisma queries in API routes must include user/account filtering (e.g., emailAccountId or userId in WHERE clauses) to prevent unauthorized data access.
All parameters used in API routes must be validated before use; do not use parameters from 'params' or request bodies directly in queries without validation.
Request bodies in API routes should use Zod schemas for validation.
API routes should only return necessary fields using Prisma's 'select' and must not include sensitive data in error messages.
Error messages in API routes must not reveal internal details; use generic errors and SafeError for user-facing errors.
All QStash endpoints (API routes called via publishToQstash or publishToQstashQueue) must use verifySignatureAppRouter to verify request authenticity.
All cron endpoints in API routes must use hasCronSecret or hasPostCronSecret for authentication.
Do not hardcode weak or plaintext secrets in API route files; secrets must not be directly assigned as string literals.
Review all new withError usage in API routes to ensure custom authentication is implemented where required.Files:
apps/web/app/api/user/categorize/senders/batch/handle-batch.tsapps/web/app/api/user/deep-clean/progress/route.tsapps/web/app/api/webhooks/deep-clean/mark-read/route.tsapps/web/app/api/webhooks/deep-clean/archive/route.tsapps/web/app/api/user/deep-clean/senders/route.tsapps/web/utils/actions/**/*.ts
📄 CodeRabbit inference engine (apps/web/CLAUDE.md)
apps/web/utils/actions/**/*.ts: Use server actions for all mutations (create/update/delete operations)
next-safe-actionprovides centralized error handling
Use Zod schemas for validation on both client and server
UserevalidatePathin server actions for cache invalidation
apps/web/utils/actions/**/*.ts: Use server actions (withnext-safe-action) for all mutations (create/update/delete operations); do NOT use POST API routes for mutations.
UserevalidatePathin server actions to invalidate cache after mutations.Files:
apps/web/utils/actions/deep-clean.tsapps/web/utils/actions/*.ts
📄 CodeRabbit inference engine (.cursor/rules/server-actions.mdc)
apps/web/utils/actions/*.ts: Implement all server actions using thenext-safe-actionlibrary for type safety, input validation, context management, and error handling. Refer toapps/web/utils/actions/safe-action.tsfor client definitions (actionClient,actionClientUser,adminActionClient).
UseactionClientUserwhen only authenticated user context (userId) is needed.
UseactionClientwhen both authenticated user context and a specificemailAccountIdare needed. TheemailAccountIdmust be bound when calling the action from the client.
UseadminActionClientfor actions restricted to admin users.
Access necessary context (likeuserId,emailAccountId, etc.) provided by the safe action client via thectxobject in the.action()handler.
Server Actions are strictly for mutations (operations that change data, e.g., creating, updating, deleting). Do NOT use Server Actions for data fetching (GET operations). For data fetching, use dedicated GET API Routes combined with SWR Hooks.
UseSafeErrorfor expected/handled errors within actions if needed.next-safe-actionprovides centralized error handling.
Use the.metadata({ name: "actionName" })method to provide a meaningful name for monitoring. Sentry instrumentation is automatically applied viawithServerActionInstrumentationwithin the safe action clients.
If an action modifies data displayed elsewhere, userevalidatePathorrevalidateTagfromnext/cachewithin the action handler as needed.Server action files must start with
use serverFiles:
apps/web/utils/actions/deep-clean.tsapps/web/app/api/**/route.ts
📄 CodeRabbit inference engine (apps/web/CLAUDE.md)
apps/web/app/api/**/route.ts: UsewithAuthfor user-level operations
UsewithEmailAccountfor email-account-level operations
Do NOT use POST API routes for mutations - use server actions instead
No need for try/catch in GET routes when using middleware
Export response types from GET routes
apps/web/app/api/**/route.ts: Wrap all GET API route handlers withwithAuthorwithEmailAccountmiddleware for authentication and authorization.
Export response types from GET API routes for type-safe client usage.
Do not use try/catch in GET API routes when using authentication middleware; rely on centralized error handling.Files:
apps/web/app/api/user/deep-clean/progress/route.tsapps/web/app/api/webhooks/deep-clean/mark-read/route.tsapps/web/app/api/webhooks/deep-clean/archive/route.tsapps/web/app/api/user/deep-clean/senders/route.ts**/api/**/route.ts
📄 CodeRabbit inference engine (.cursor/rules/security.mdc)
**/api/**/route.ts: ALL API routes that handle user data MUST use appropriate authentication and authorization middleware (withAuth or withEmailAccount).
ALL database queries in API routes MUST be scoped to the authenticated user/account (e.g., include userId or emailAccountId in query filters).
Always validate that resources belong to the authenticated user before performing operations (resource ownership validation).
UsewithEmailAccountmiddleware for API routes that operate on a specific email account (i.e., use or requireemailAccountId).
UsewithAuthmiddleware for API routes that operate at the user level (i.e., use or require onlyuserId).
UsewithErrormiddleware (with proper validation) for public endpoints, custom authentication, or cron endpoints.
Cron endpoints MUST usewithErrormiddleware and validate the cron secret usinghasCronSecret(request)orhasPostCronSecret(request).
Cron endpoints MUST capture unauthorized attempts withcaptureExceptionand return a 401 status for unauthorized requests.
All parameters in API routes MUST be validated for type, format, and length before use.
Request bodies in API routes MUST be validated using Zod schemas before use.
All Prisma queries in API routes MUST only return necessary fields and never expose sensitive data.
Error messages in API routes MUST not leak internal information or sensitive data; use generic error messages and SafeError where appropriate.
API routes MUST use a consistent error response format, returning JSON with an error message and status code.
AllfindUniqueandfindFirstPrisma calls in API routes MUST include ownership filters (e.g., userId or emailAccountId).
AllfindManyPrisma calls in API routes MUST be scoped to the authenticated user's data.
Never use direct object references in API routes without ownership checks (prevent IDOR vulnerabilities).
Prevent mass assignment vulnerabilities by only allowing explicitly whitelisted fields in update operations in AP...Files:
apps/web/app/api/user/deep-clean/progress/route.tsapps/web/app/api/webhooks/deep-clean/mark-read/route.tsapps/web/app/api/webhooks/deep-clean/archive/route.tsapps/web/app/api/user/deep-clean/senders/route.tsapps/web/hooks/**/*.ts
📄 CodeRabbit inference engine (apps/web/CLAUDE.md)
Use SWR for efficient data fetching and caching
apps/web/hooks/**/*.ts: Use SWR for client-side data fetching and caching.
Callmutate()after successful mutations to refresh SWR data on the client.Files:
apps/web/hooks/useDeepClean.tsapps/web/hooks/**/*.{ts,tsx}
📄 CodeRabbit inference engine (apps/web/CLAUDE.md)
Call
mutate()after successful mutations to refresh dataFiles:
apps/web/hooks/useDeepClean.tsapps/web/hooks/**/*.{js,jsx,ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/hooks.mdc)
Place custom hooks in the
apps/web/hooks/directory.Files:
apps/web/hooks/useDeepClean.tsapps/web/hooks/use*.{js,jsx,ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/hooks.mdc)
apps/web/hooks/use*.{js,jsx,ts,tsx}: Name custom hooks with theuseprefix (e.g.,useAccounts.ts).
For fetching data from API endpoints in custom hooks, prefer usinguseSWR.
Create dedicated hooks for specific data types (e.g.,useAccounts,useLabels).
Custom hooks should encapsulate reusable stateful logic, especially for data fetching or complex UI interactions.
Keep custom hooks focused on a single responsibility.Files:
apps/web/hooks/useDeepClean.tsapps/web/components/**/*.tsx
📄 CodeRabbit inference engine (apps/web/CLAUDE.md)
Use React Hook Form with Zod validation for form handling
Use the
LoadingContentcomponent to handle loading and error states consistently in data-fetching components.Use PascalCase for components (e.g.
components/Button.tsx)Files:
apps/web/components/SideNav.tsxapps/web/components/DeepCleanGroupedTable.tsx🧠 Learnings (4)
📚 Learning: 2025-07-18T15:05:16.146Z
Learnt from: CR PR: elie222/inbox-zero#0 File: .cursor/rules/fullstack-workflow.mdc:0-0 Timestamp: 2025-07-18T15:05:16.146Z Learning: Applies to apps/web/hooks/**/*.ts : Use SWR for client-side data fetching and caching.Applied to files:
apps/web/hooks/useDeepClean.ts📚 Learning: 2025-07-18T15:04:30.467Z
Learnt from: CR PR: elie222/inbox-zero#0 File: apps/web/CLAUDE.md:0-0 Timestamp: 2025-07-18T15:04:30.467Z Learning: Applies to apps/web/hooks/**/*.ts : Use SWR for efficient data fetching and cachingApplied to files:
apps/web/hooks/useDeepClean.ts📚 Learning: 2025-07-18T15:05:41.705Z
Learnt from: CR PR: elie222/inbox-zero#0 File: .cursor/rules/hooks.mdc:0-0 Timestamp: 2025-07-18T15:05:41.705Z Learning: Applies to apps/web/hooks/use*.{js,jsx,ts,tsx} : For fetching data from API endpoints in custom hooks, prefer using `useSWR`.Applied to files:
apps/web/hooks/useDeepClean.ts📚 Learning: 2025-07-19T17:50:22.078Z
Learnt from: CR PR: elie222/inbox-zero#0 File: .cursor/rules/ui-components.mdc:0-0 Timestamp: 2025-07-19T17:50:22.078Z Learning: Applies to {components,app}/**/*.tsx : For API get requests to server use the `swr` packageApplied to files:
apps/web/hooks/useDeepClean.ts🧬 Code graph analysis (18)
apps/web/app/(app)/[emailAccountId]/deep-clean/DeepCleanContent.tsx (8)
apps/web/providers/EmailAccountProvider.tsx (1)
useAccount(79-89)apps/web/hooks/useDeepClean.ts (1)
useDeepCleanSenders(5-7)apps/web/utils/actions/deep-clean.ts (1)
categorizeMoreSendersAction(108-229)apps/web/components/LoadingContent.tsx (1)
LoadingContent(13-27)apps/web/components/ClientOnly.tsx (1)
ClientOnly(5-13)apps/web/app/(app)/[emailAccountId]/deep-clean/BulkOperationProgress.tsx (1)
BulkOperationProgress(7-63)apps/web/app/(app)/[emailAccountId]/deep-clean/CategorizeProgress.tsx (1)
CategorizeSendersProgress(19-80)apps/web/components/DeepCleanGroupedTable.tsx (1)
DeepCleanGroupedTable(61-429)apps/web/app/(app)/[emailAccountId]/deep-clean/CategorizeWithAiButton.tsx (7)
apps/web/providers/EmailAccountProvider.tsx (1)
useAccount(79-89)apps/web/components/PremiumAlert.tsx (2)
usePremium(14-42)PremiumTooltip(150-164)apps/web/app/(app)/premium/PremiumModal.tsx (1)
usePremiumModal(5-25)apps/web/app/(app)/[emailAccountId]/deep-clean/CategorizeProgress.tsx (1)
useCategorizeProgress(12-17)apps/web/utils/premium/index.ts (1)
hasAiAccess(87-101)apps/web/utils/actions/categorize.ts (1)
bulkCategorizeSendersAction(29-101)apps/web/app/(app)/[emailAccountId]/smart-categories/CategorizeWithAiButton.tsx (1)
CategorizeWithAiButton(15-81)apps/web/app/(app)/[emailAccountId]/deep-clean/page.tsx (4)
apps/web/components/PageWrapper.tsx (1)
PageWrapper(3-20)apps/web/app/(app)/[emailAccountId]/PermissionsCheck.tsx (1)
PermissionsCheck(12-33)apps/web/components/PageHeader.tsx (1)
PageHeader(14-34)apps/web/app/(app)/[emailAccountId]/deep-clean/DeepCleanContent.tsx (1)
DeepCleanContent(16-100)apps/web/utils/email.test.ts (1)
apps/web/utils/email.ts (1)
extractNameFromEmail(10-38)apps/web/app/api/user/categorize/senders/batch/handle-batch.ts (1)
apps/web/utils/categorize/senders/categorize.ts (1)
getCategories(181-192)apps/web/utils/actions/deep-clean.ts (5)
apps/web/utils/actions/safe-action.ts (1)
actionClient(62-113)apps/web/utils/user/validate.ts (1)
validateUserAndAiAccess(5-41)apps/web/utils/error.ts (1)
SafeError(86-96)apps/web/utils/upstash/bulk-operations.ts (2)
publishArchiveCategoryQueue(20-57)publishMarkAsReadCategoryQueue(62-99)apps/web/utils/upstash/deep-clean-categorization.ts (1)
getTopSendersForDeepClean(51-108)apps/web/app/(app)/[emailAccountId]/deep-clean/BulkOperationProgress.tsx (2)
apps/web/app/api/user/deep-clean/progress/route.ts (1)
BulkOperationProgress(11-13)apps/web/hooks/useDeepClean.ts (1)
useBulkOperationProgress(9-16)apps/web/utils/categorize/senders/categorize.ts (1)
apps/web/utils/priority.ts (2)
PriorityLevel(1-1)priorityToNumber(9-11)apps/web/app/api/user/deep-clean/progress/route.ts (4)
apps/web/utils/logger.ts (1)
createScopedLogger(17-80)apps/web/app/(app)/[emailAccountId]/deep-clean/BulkOperationProgress.tsx (1)
BulkOperationProgress(7-63)apps/web/utils/redis/bulk-operation-progress.ts (2)
getBulkOperationProgress(31-41)getAllBulkOperations(116-139)apps/web/utils/middleware.ts (1)
withEmailAccount(293-298)apps/web/app/api/webhooks/deep-clean/mark-read/route.ts (5)
apps/web/utils/logger.ts (1)
createScopedLogger(17-80)apps/web/utils/user/validate.ts (1)
validateUserAndAiAccess(5-41)apps/web/utils/error.ts (1)
SafeError(86-96)apps/web/utils/email/provider.ts (1)
createEmailProvider(13-29)apps/web/utils/redis/bulk-operation-progress.ts (2)
createBulkOperation(43-73)updateBulkOperationProgress(75-114)apps/web/utils/upstash/deep-clean-categorization.ts (4)
apps/web/app/api/outlook/webhook/logger.ts (1)
logger(3-3)apps/web/utils/logger.ts (1)
createScopedLogger(17-80)apps/web/env.ts (1)
env(16-242)apps/web/utils/upstash/index.ts (1)
publishToQstashQueue(56-78)apps/web/app/(app)/[emailAccountId]/deep-clean/CreateCategoryButton.tsx (3)
apps/web/hooks/useModal.tsx (1)
useModal(3-9)apps/web/providers/EmailAccountProvider.tsx (1)
useAccount(79-89)apps/web/utils/actions/categorize.ts (1)
createCategoryAction(186-198)apps/web/utils/upstash/bulk-operations.ts (3)
apps/web/utils/logger.ts (1)
createScopedLogger(17-80)apps/web/env.ts (1)
env(16-242)apps/web/utils/upstash/index.ts (1)
publishToQstashQueue(56-78)apps/web/hooks/useDeepClean.ts (3)
apps/web/app/api/user/deep-clean/senders/route.ts (1)
DeepCleanSendersResponse(10-12)apps/web/app/(app)/[emailAccountId]/deep-clean/BulkOperationProgress.tsx (1)
BulkOperationProgress(7-63)apps/web/app/api/user/deep-clean/progress/route.ts (1)
BulkOperationProgress(11-13)apps/web/app/(app)/[emailAccountId]/deep-clean/Uncategorized.tsx (14)
apps/web/utils/category.server.ts (1)
CategoryWithRules(7-14)apps/web/components/PremiumAlert.tsx (2)
usePremium(14-42)PremiumTooltip(150-164)apps/web/app/(app)/premium/PremiumModal.tsx (1)
usePremiumModal(5-25)apps/web/store/ai-categorize-sender-queue.ts (3)
useHasProcessingItems(61-63)pushToAiCategorizeSenderQueueAtom(19-37)stopAiCategorizeSenderQueue(39-42)apps/web/providers/EmailAccountProvider.tsx (1)
useAccount(79-89)apps/web/components/LoadingContent.tsx (1)
LoadingContent(13-27)apps/web/components/TopBar.tsx (1)
TopBar(9-21)apps/web/components/TooltipExplanation.tsx (1)
TooltipExplanation(33-60)apps/web/components/ClientOnly.tsx (1)
ClientOnly(5-13)apps/web/components/GroupedTable.tsx (1)
SendersTable(294-371)apps/web/components/Loading.tsx (1)
ButtonLoader(15-17)apps/web/components/Typography.tsx (1)
SectionDescription(127-127)apps/web/components/Toggle.tsx (1)
Toggle(18-50)apps/web/utils/actions/categorize.ts (1)
setAutoCategorizeAction(258-271)apps/web/app/api/webhooks/deep-clean/archive/route.ts (6)
apps/web/utils/logger.ts (1)
createScopedLogger(17-80)apps/web/app/api/webhooks/deep-clean/mark-read/route.ts (1)
POST(18-187)apps/web/utils/user/validate.ts (1)
validateUserAndAiAccess(5-41)apps/web/utils/error.ts (1)
SafeError(86-96)apps/web/utils/email/provider.ts (1)
createEmailProvider(13-29)apps/web/utils/redis/bulk-operation-progress.ts (2)
createBulkOperation(43-73)updateBulkOperationProgress(75-114)apps/web/app/api/user/deep-clean/senders/route.ts (4)
apps/web/utils/logger.ts (1)
createScopedLogger(17-80)apps/web/utils/category.server.ts (1)
getUserCategoriesWithRules(27-42)apps/web/utils/priority.ts (1)
numberToPriority(13-24)apps/web/utils/middleware.ts (1)
withEmailAccount(293-298)apps/web/components/DeepCleanGroupedTable.tsx (9)
apps/web/utils/category.server.ts (1)
CategoryWithRules(7-14)apps/web/providers/EmailAccountProvider.tsx (1)
useAccount(79-89)apps/web/utils/url.ts (2)
getGmailSearchUrl(80-84)getEmailUrl(46-53)apps/web/store/archive-sender-queue.ts (2)
addToArchiveSenderQueue(19-111)useArchiveSenderStatus(118-121)apps/web/utils/actions/categorize.ts (1)
changeSenderCategoryAction(131-152)apps/web/utils/actions/deep-clean.ts (1)
markCategoryAsReadAction(231-310)apps/web/hooks/useThreads.ts (1)
useThreads(8-28)apps/web/components/ViewEmailButton.tsx (1)
ViewEmailButton(8-39)apps/web/utils/date.ts (1)
formatShortDate(22-53)⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: cubic · AI code reviewer
- GitHub Check: test
| // Hide completed operations after 5 seconds | ||
| if (isCompleted) { | ||
| setTimeout(() => { | ||
| // This will be handled by React's re-render when the operation is removed from Redis | ||
| }, 5000); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove render-time setTimeout side effect.
Calling setTimeout directly in render spins up a new timer on every render pass, even under the same completion state, leaking timers and causing unnecessary work. Move this logic into an effect or drop it entirely if Redis cleanup already controls visibility.
Apply this diff to eliminate the render-side effect:
- // Hide completed operations after 5 seconds
- if (isCompleted) {
- setTimeout(() => {
- // This will be handled by React's re-render when the operation is removed from Redis
- }, 5000);
- }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Hide completed operations after 5 seconds | |
| if (isCompleted) { | |
| setTimeout(() => { | |
| // This will be handled by React's re-render when the operation is removed from Redis | |
| }, 5000); | |
| } |
🤖 Prompt for AI Agents
In apps/web/app/(app)/[emailAccountId]/deep-clean/BulkOperationProgress.tsx
around lines 33-38, remove the render-time setTimeout call (which creates a new
timer on every render) and instead move that logic into a useEffect that watches
isCompleted: when isCompleted transitions to true start a single timeout to
hide/remove the operation after 5s and return a cleanup function to clear the
timeout if isCompleted changes or the component unmounts; alternatively, if
Redis-driven cleanup already controls visibility, delete the timeout logic
entirely.
| onClick={async () => { | ||
| if (isCategorizing) return; | ||
| toast.promise( | ||
| async () => { | ||
| setIsCategorizing(true); | ||
| setIsBulkCategorizing(true); | ||
| const result = | ||
| await bulkCategorizeSendersAction(emailAccountId); | ||
|
|
||
| if (result?.serverError) { | ||
| setIsCategorizing(false); | ||
| throw new Error(result.serverError); | ||
| } | ||
|
|
||
| setIsCategorizing(false); | ||
|
|
||
| return result?.data?.totalUncategorizedSenders || 0; | ||
| }, | ||
| { | ||
| loading: "Categorizing senders... This might take a while.", | ||
| success: (totalUncategorizedSenders) => { | ||
| return totalUncategorizedSenders | ||
| ? `Categorizing ${totalUncategorizedSenders} senders...` | ||
| : "There are no more senders to categorize."; | ||
| }, | ||
| error: (err) => { | ||
| return `Error categorizing senders: ${err.message}`; | ||
| }, | ||
| }, | ||
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reset categorization flags when the action fails
We set setIsBulkCategorizing(true) and flip isCategorizing before the request, but we never reset them when the action fails or throws (network error, serverError branch). That leaves the button stuck in a loading/“in progress” state and the progress UI permanently true. Use a try/finally to always clear isCategorizing, and reset the bulk flag on failure before rethrowing.
- toast.promise(
- async () => {
- setIsCategorizing(true);
- setIsBulkCategorizing(true);
- const result =
- await bulkCategorizeSendersAction(emailAccountId);
-
- if (result?.serverError) {
- setIsCategorizing(false);
- throw new Error(result.serverError);
- }
-
- setIsCategorizing(false);
-
- return result?.data?.totalUncategorizedSenders || 0;
- },
+ toast.promise(
+ async () => {
+ setIsCategorizing(true);
+ setIsBulkCategorizing(true);
+ try {
+ const result =
+ await bulkCategorizeSendersAction(emailAccountId);
+
+ if (result?.serverError) {
+ throw new Error(result.serverError);
+ }
+
+ return result?.data?.totalUncategorizedSenders || 0;
+ } catch (error) {
+ setIsBulkCategorizing(false);
+ throw error;
+ } finally {
+ setIsCategorizing(false);
+ }
+ },📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| onClick={async () => { | |
| if (isCategorizing) return; | |
| toast.promise( | |
| async () => { | |
| setIsCategorizing(true); | |
| setIsBulkCategorizing(true); | |
| const result = | |
| await bulkCategorizeSendersAction(emailAccountId); | |
| if (result?.serverError) { | |
| setIsCategorizing(false); | |
| throw new Error(result.serverError); | |
| } | |
| setIsCategorizing(false); | |
| return result?.data?.totalUncategorizedSenders || 0; | |
| }, | |
| { | |
| loading: "Categorizing senders... This might take a while.", | |
| success: (totalUncategorizedSenders) => { | |
| return totalUncategorizedSenders | |
| ? `Categorizing ${totalUncategorizedSenders} senders...` | |
| : "There are no more senders to categorize."; | |
| }, | |
| error: (err) => { | |
| return `Error categorizing senders: ${err.message}`; | |
| }, | |
| }, | |
| ); | |
| onClick={async () => { | |
| if (isCategorizing) return; | |
| toast.promise( | |
| async () => { | |
| setIsCategorizing(true); | |
| setIsBulkCategorizing(true); | |
| try { | |
| const result = | |
| await bulkCategorizeSendersAction(emailAccountId); | |
| if (result?.serverError) { | |
| throw new Error(result.serverError); | |
| } | |
| return result?.data?.totalUncategorizedSenders || 0; | |
| } catch (error) { | |
| setIsBulkCategorizing(false); | |
| throw error; | |
| } finally { | |
| setIsCategorizing(false); | |
| } | |
| }, | |
| { | |
| loading: "Categorizing senders... This might take a while.", | |
| success: (totalUncategorizedSenders) => { | |
| return totalUncategorizedSenders | |
| ? `Categorizing ${totalUncategorizedSenders} senders...` | |
| : "There are no more senders to categorize."; | |
| }, | |
| error: (err) => { | |
| return `Error categorizing senders: ${err.message}`; | |
| }, | |
| }, | |
| ); |
🤖 Prompt for AI Agents
In apps/web/app/(app)/[emailAccountId]/deep-clean/CategorizeWithAiButton.tsx
around lines 37-66, wrap the async action in a try/finally (with an optional
catch) so the state flags are always cleared: set both setIsCategorizing(false)
and setIsBulkCategorizing(false) in a finally block to ensure the button and
progress UI are reset after success or failure; if you need to preserve
different behavior on failure, clear setIsBulkCategorizing(false) in the catch
before rethrowing the error and still clear setIsCategorizing(false) in finally.
|
|
||
| import { useCallback, useRef, useEffect } from "react"; | ||
| import { ClientOnly } from "@/components/ClientOnly"; | ||
| import { CategorizeSendersProgress } from "@/app/(app)/[emailAccountId]/smart-categories/CategorizeProgress"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix the CategorizeSendersProgress import path
CategorizeSendersProgress now lives under the deep-clean module. Importing it from smart-categories will fail at build/runtime, breaking the page. Point the import at the new deep-clean location.
-import { CategorizeSendersProgress } from "@/app/(app)/[emailAccountId]/smart-categories/CategorizeProgress";
+import { CategorizeSendersProgress } from "@/app/(app)/[emailAccountId]/deep-clean/CategorizeProgress";📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| import { CategorizeSendersProgress } from "@/app/(app)/[emailAccountId]/smart-categories/CategorizeProgress"; | |
| import { CategorizeSendersProgress } from "@/app/(app)/[emailAccountId]/deep-clean/CategorizeProgress"; |
🤖 Prompt for AI Agents
In apps/web/app/(app)/[emailAccountId]/deep-clean/DeepCleanContent.tsx around
line 5, the import for CategorizeSendersProgress points to the old
smart-categories path which will break; update the import to the new deep-clean
module path (i.e., import CategorizeSendersProgress from the deep-clean
location) so the component imports from the module that now owns it.
| } catch (error) { | ||
| toast.error("Failed to categorize senders"); | ||
| console.error("Categorize more error:", error); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
Remove the disallowed console call
The front-end guidelines forbid console.* usage. Drop the console.error (or replace it with the project’s approved error reporting utility) and rely on the toast already shown to surface failures.
- } catch (error) {
- toast.error("Failed to categorize senders");
- console.error("Categorize more error:", error);
- }
+ } catch (error) {
+ toast.error("Failed to categorize senders");
+ // TODO: optionally capture error with approved client-side logger
+ }🤖 Prompt for AI Agents
In apps/web/app/(app)/[emailAccountId]/deep-clean/DeepCleanContent.tsx around
lines 38 to 41, remove the disallowed console.error call and either drop it
entirely or replace it with the project’s approved error reporting utility
(e.g., reportError or logger.error) while keeping the existing toast.error;
update imports if you add the reporting utility and ensure no console.* calls
remain in this catch block.
| function AutoCategorizeToggle({ | ||
| autoCategorizeSenders, | ||
| emailAccountId, | ||
| }: { | ||
| autoCategorizeSenders: boolean; | ||
| emailAccountId: string; | ||
| }) { | ||
| return ( | ||
| <Toggle | ||
| name="autoCategorizeSenders" | ||
| label="Auto categorize" | ||
| enabled={autoCategorizeSenders} | ||
| onChange={async (enabled) => { | ||
| await setAutoCategorizeAction(emailAccountId, { | ||
| autoCategorizeSenders: enabled, | ||
| }); | ||
| }} | ||
| /> | ||
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Auto-categorize toggle never updates
Toggle is controlled via enabled={autoCategorizeSenders}, but that prop never changes after the server action runs. The Switch immediately snaps back, so the user can’t tell whether auto-categorize is on or off without a full refresh. Track local state (or revalidate data) and roll it back on error so the UI mirrors the saved setting.
-function AutoCategorizeToggle({
- autoCategorizeSenders,
- emailAccountId,
-}: {
- autoCategorizeSenders: boolean;
- emailAccountId: string;
-}) {
- return (
- <Toggle
- name="autoCategorizeSenders"
- label="Auto categorize"
- enabled={autoCategorizeSenders}
- onChange={async (enabled) => {
- await setAutoCategorizeAction(emailAccountId, {
- autoCategorizeSenders: enabled,
- });
- }}
- />
- );
-}
+function AutoCategorizeToggle({
+ autoCategorizeSenders,
+ emailAccountId,
+}: {
+ autoCategorizeSenders: boolean;
+ emailAccountId: string;
+}) {
+ const [enabled, setEnabled] = useState(autoCategorizeSenders);
+
+ useEffect(() => {
+ setEnabled(autoCategorizeSenders);
+ }, [autoCategorizeSenders]);
+
+ return (
+ <Toggle
+ name="autoCategorizeSenders"
+ label="Auto categorize"
+ enabled={enabled}
+ onChange={async (next) => {
+ setEnabled(next);
+ const result = await setAutoCategorizeAction(emailAccountId, {
+ autoCategorizeSenders: next,
+ });
+ if (result?.serverError) {
+ setEnabled(!next);
+ toastError({ description: result.serverError });
+ } else {
+ toastSuccess({
+ description: next
+ ? "Auto categorize enabled"
+ : "Auto categorize disabled",
+ });
+ }
+ }}
+ />
+ );
+}Committable suggestion skipped: line range outside the PR's diff.
| export async function POST(request: Request) { | ||
| try { | ||
| const body = await request.json(); | ||
| const { emailAccountId, operationId, category, senders } = body; | ||
|
|
||
| if (!emailAccountId || !operationId || !category || !senders) { | ||
| return NextResponse.json( | ||
| { error: "Missing required fields" }, | ||
| { status: 400 }, | ||
| ); | ||
| } | ||
|
|
||
| logger.info("Starting archive operation", { | ||
| emailAccountId, | ||
| operationId, | ||
| category, | ||
| senderCount: senders.length, | ||
| }); | ||
|
|
||
| // Validate user access | ||
| await validateUserAndAiAccess({ emailAccountId }); | ||
|
|
||
| // Get email account details | ||
| const emailAccountWithAccount = await prisma.emailAccount.findUnique({ | ||
| where: { id: emailAccountId }, | ||
| select: { | ||
| email: true, | ||
| account: { | ||
| select: { | ||
| provider: true, | ||
| }, | ||
| }, | ||
| }, | ||
| }); | ||
|
|
||
| const account = emailAccountWithAccount?.account; | ||
| const ownerEmail = emailAccountWithAccount?.email; | ||
| if (!account) throw new SafeError("No account found"); | ||
| if (!ownerEmail) throw new SafeError("No email found"); | ||
|
|
||
| // Create email provider (works for both Gmail and Outlook) | ||
| const emailProvider = await createEmailProvider({ | ||
| emailAccountId, | ||
| provider: account.provider, | ||
| }); | ||
|
|
||
| // Get all thread IDs for these senders | ||
| const allThreadIds: string[] = []; | ||
| for (const sender of senders) { | ||
| try { | ||
| const threads = await emailProvider.getThreadsFromSenderWithSubject( | ||
| sender, | ||
| 1000, | ||
| ); | ||
| allThreadIds.push(...threads.map((t) => t.id)); | ||
| } catch (error) { | ||
| logger.warn("Failed to get threads for sender", { | ||
| sender, | ||
| error, | ||
| }); | ||
| // Continue with other senders even if one fails | ||
| } | ||
| } | ||
|
|
||
| if (allThreadIds.length === 0) { | ||
| logger.info("No threads found to archive", { | ||
| emailAccountId, | ||
| operationId, | ||
| }); | ||
| return NextResponse.json({ | ||
| ok: true, | ||
| message: "No threads found to archive", | ||
| operationId, | ||
| }); | ||
| } | ||
|
|
||
| // Create progress tracker | ||
| await createBulkOperation({ | ||
| emailAccountId, | ||
| operationId, | ||
| operationType: "archive", | ||
| categoryOrSender: category, | ||
| totalItems: allThreadIds.length, | ||
| }); | ||
|
|
||
| // Update status to processing | ||
| await updateBulkOperationProgress({ | ||
| emailAccountId, | ||
| operationId, | ||
| status: "processing", | ||
| }); | ||
|
|
||
| let successCount = 0; | ||
| let errorCount = 0; | ||
|
|
||
| // Process threads in batches | ||
| const BATCH_SIZE = 50; | ||
| for (let i = 0; i < allThreadIds.length; i += BATCH_SIZE) { | ||
| const batch = allThreadIds.slice(i, i + BATCH_SIZE); | ||
|
|
||
| await Promise.all( | ||
| batch.map(async (threadId) => { | ||
| try { | ||
| await emailProvider.archiveThread(threadId, ownerEmail); | ||
| successCount++; | ||
|
|
||
| // Update progress every 10 threads | ||
| if (successCount % 10 === 0) { | ||
| await updateBulkOperationProgress({ | ||
| emailAccountId, | ||
| operationId, | ||
| incrementCompleted: 10, | ||
| }); | ||
| } | ||
| } catch (error) { | ||
| errorCount++; | ||
| logger.warn("Failed to archive thread", { | ||
| threadId, | ||
| error, | ||
| }); | ||
| } | ||
| }), | ||
| ); | ||
|
|
||
| logger.info("Batch archived", { | ||
| operationId, | ||
| batchNumber: Math.floor(i / BATCH_SIZE) + 1, | ||
| totalBatches: Math.ceil(allThreadIds.length / BATCH_SIZE), | ||
| successCount, | ||
| errorCount, | ||
| }); | ||
| } | ||
|
|
||
| // Final progress update | ||
| await updateBulkOperationProgress({ | ||
| emailAccountId, | ||
| operationId, | ||
| incrementCompleted: successCount % 10, // Update any remaining | ||
| incrementFailed: errorCount, | ||
| status: "completed", | ||
| }); | ||
|
|
||
| logger.info("Archive operation completed", { | ||
| emailAccountId, | ||
| operationId, | ||
| totalThreads: allThreadIds.length, | ||
| successCount, | ||
| errorCount, | ||
| }); | ||
|
|
||
| return NextResponse.json({ | ||
| ok: true, | ||
| message: `Archived ${successCount} emails${errorCount > 0 ? ` (${errorCount} failed)` : ""}`, | ||
| operationId, | ||
| successCount, | ||
| errorCount, | ||
| }); | ||
| } catch (error) { | ||
| logger.error("Archive operation error", { error }); | ||
|
|
||
| if (error instanceof SafeError) { | ||
| return NextResponse.json({ error: error.message }, { status: 400 }); | ||
| } | ||
|
|
||
| return NextResponse.json( | ||
| { error: "Internal server error" }, | ||
| { status: 500 }, | ||
| ); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Protect the webhook with QStash signature verification
This route is invoked from publishArchiveCategoryQueue, so it must verify the QStash signature. Right now anyone can POST to this endpoint and trigger an archive run for any account. Please wrap the handler with verifySignatureAppRouter (as per the API/QStash guideline) so only QStash-authenticated requests are processed.
-import { NextResponse } from "next/server";
+import { NextResponse } from "next/server";
+import { verifySignatureAppRouter } from "@upstash/qstash/nextjs";
…
-export async function POST(request: Request) {
+export const POST = verifySignatureAppRouter(async (request) => {
try {
const body = await request.json();
@@
- return NextResponse.json(
- { error: "Internal server error" },
- { status: 500 },
- );
+ return NextResponse.json(
+ { error: "Internal server error" },
+ { status: 500 },
+ );
}
-}
+});As per coding guidelines
| export async function POST(request: Request) { | ||
| try { | ||
| const body = await request.json(); | ||
| const { emailAccountId, operationId, category, senders } = body; | ||
|
|
||
| if (!emailAccountId || !operationId || !category || !senders) { | ||
| return NextResponse.json( | ||
| { error: "Missing required fields" }, | ||
| { status: 400 }, | ||
| ); | ||
| } | ||
|
|
||
| logger.info("Starting mark-as-read operation", { | ||
| emailAccountId, | ||
| operationId, | ||
| category, | ||
| senderCount: senders.length, | ||
| }); | ||
|
|
||
| // Validate user access | ||
| await validateUserAndAiAccess({ emailAccountId }); | ||
|
|
||
| // Get email account details | ||
| const emailAccountWithAccount = await prisma.emailAccount.findUnique({ | ||
| where: { id: emailAccountId }, | ||
| select: { | ||
| email: true, | ||
| account: { | ||
| select: { | ||
| provider: true, | ||
| }, | ||
| }, | ||
| }, | ||
| }); | ||
|
|
||
| const account = emailAccountWithAccount?.account; | ||
| const ownerEmail = emailAccountWithAccount?.email; | ||
| if (!account) throw new SafeError("No account found"); | ||
| if (!ownerEmail) throw new SafeError("No email found"); | ||
|
|
||
| // Create email provider (works for both Gmail and Outlook) | ||
| const emailProvider = await createEmailProvider({ | ||
| emailAccountId, | ||
| provider: account.provider, | ||
| }); | ||
|
|
||
| // Get all thread IDs for these senders | ||
| const allThreadIds: string[] = []; | ||
| for (const sender of senders) { | ||
| try { | ||
| const threads = await emailProvider.getThreadsFromSenderWithSubject( | ||
| sender, | ||
| 1000, | ||
| ); | ||
| allThreadIds.push(...threads.map((t) => t.id)); | ||
| } catch (error) { | ||
| logger.warn("Failed to get threads for sender", { | ||
| sender, | ||
| error, | ||
| }); | ||
| // Continue with other senders even if one fails | ||
| } | ||
| } | ||
|
|
||
| if (allThreadIds.length === 0) { | ||
| logger.info("No threads found to mark as read", { | ||
| emailAccountId, | ||
| operationId, | ||
| }); | ||
| return NextResponse.json({ | ||
| ok: true, | ||
| message: "No threads found to mark as read", | ||
| operationId, | ||
| }); | ||
| } | ||
|
|
||
| // Create progress tracker | ||
| await createBulkOperation({ | ||
| emailAccountId, | ||
| operationId, | ||
| operationType: "mark-read", | ||
| categoryOrSender: category, | ||
| totalItems: allThreadIds.length, | ||
| }); | ||
|
|
||
| // Update status to processing | ||
| await updateBulkOperationProgress({ | ||
| emailAccountId, | ||
| operationId, | ||
| status: "processing", | ||
| }); | ||
|
|
||
| let successCount = 0; | ||
| let errorCount = 0; | ||
|
|
||
| // Process threads in batches | ||
| const BATCH_SIZE = 50; | ||
| for (let i = 0; i < allThreadIds.length; i += BATCH_SIZE) { | ||
| const batch = allThreadIds.slice(i, i + BATCH_SIZE); | ||
|
|
||
| await Promise.all( | ||
| batch.map(async (threadId) => { | ||
| try { | ||
| await emailProvider.markReadThread(threadId, true); | ||
| successCount++; | ||
|
|
||
| // Update progress every 10 threads | ||
| if (successCount % 10 === 0) { | ||
| await updateBulkOperationProgress({ | ||
| emailAccountId, | ||
| operationId, | ||
| incrementCompleted: 10, | ||
| }); | ||
| } | ||
| } catch (error) { | ||
| errorCount++; | ||
| logger.warn("Failed to mark thread as read", { | ||
| threadId, | ||
| error, | ||
| }); | ||
| } | ||
| }), | ||
| ); | ||
|
|
||
| logger.info("Batch marked as read", { | ||
| operationId, | ||
| batchNumber: Math.floor(i / BATCH_SIZE) + 1, | ||
| totalBatches: Math.ceil(allThreadIds.length / BATCH_SIZE), | ||
| successCount, | ||
| errorCount, | ||
| }); | ||
| } | ||
|
|
||
| // Final progress update | ||
| await updateBulkOperationProgress({ | ||
| emailAccountId, | ||
| operationId, | ||
| incrementCompleted: successCount % 10, // Update any remaining | ||
| incrementFailed: errorCount, | ||
| status: "completed", | ||
| }); | ||
|
|
||
| logger.info("Mark-as-read operation completed", { | ||
| emailAccountId, | ||
| operationId, | ||
| totalThreads: allThreadIds.length, | ||
| successCount, | ||
| errorCount, | ||
| }); | ||
|
|
||
| return NextResponse.json({ | ||
| ok: true, | ||
| message: `Marked ${successCount} emails as read${errorCount > 0 ? ` (${errorCount} failed)` : ""}`, | ||
| operationId, | ||
| successCount, | ||
| errorCount, | ||
| }); | ||
| } catch (error) { | ||
| logger.error("Mark-as-read operation error", { error }); | ||
|
|
||
| if (error instanceof SafeError) { | ||
| return NextResponse.json({ error: error.message }, { status: 400 }); | ||
| } | ||
|
|
||
| return NextResponse.json( | ||
| { error: "Internal server error" }, | ||
| { status: 500 }, | ||
| ); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Verify QStash webhook signatures
This webhook runs without verifySignatureAppRouter, so anyone hitting the URL can trigger mark-read operations. QStash endpoints must validate the signature before reading the body or touching user data.
Follow the existing webhook pattern (e.g. archive route) by wrapping the handler with verifySignatureAppRouter:
-import { NextResponse } from "next/server";
+import { NextResponse } from "next/server";
+import { verifySignatureAppRouter } from "@/utils/qstash";
…
-export async function POST(request: Request) {
+export const POST = verifySignatureAppRouter(async (request) => {
try {
- const body = await request.json();
+ const body = await request.json();
…
- }
-}
+ }
+});This blocks unauthorized calls and keeps the route compliant. As per coding guidelines.
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
apps/web/app/api/webhooks/deep-clean/mark-read/route.ts lines 18-187: the route
currently parses the request and performs actions without verifying QStash
signatures; wrap the handler with verifySignatureAppRouter (import it from the
existing webhook utilities) so the signature is validated before reading the
body or touching user data, move request.json() and all downstream logic inside
the verified handler, and return a 401/400 response on invalid signature to
block unauthorized calls.
| <TableRow key={thread.id} className="bg-muted/50"> | ||
| <TableCell className="py-3"> | ||
| <ViewEmailButton threadId={thread.id} messageId={thread.id} /> | ||
| </TableCell> | ||
| <TableCell className="py-3"> | ||
| <Link | ||
| href={getEmailUrl(thread.id, userEmail, provider)} | ||
| target="_blank" | ||
| className="hover:underline" | ||
| > | ||
| {subject} | ||
| </Link> | ||
| </TableCell> | ||
| <TableCell className="py-3"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pass the real messageId to ViewEmailButton
ViewEmailButton expects the individual message ID (see its showEmail({ threadId, messageId }) usage). We’re currently sending the thread ID for both arguments, so Gmail users can’t open the message. Use the first message’s ID instead.
- <ViewEmailButton threadId={thread.id} messageId={thread.id} />
+ <ViewEmailButton
+ threadId={thread.id}
+ messageId={firstMessage.id}
+ />Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In apps/web/components/DeepCleanGroupedTable.tsx around lines 627 to 640,
ViewEmailButton is being passed the thread ID for both threadId and messageId;
replace the messageId argument with the real first message's ID from the thread
(e.g. use the first entry like thread.messages[0].id or thread.messageIds[0]
depending on the data shape) so showEmail receives { threadId, messageId:
<firstMessageId> } and Gmail users can open the individual message.
apps/web/utils/actions/deep-clean.ts
Outdated
| // Get all senders in this category | ||
| const senders = await prisma.newsletter.findMany({ | ||
| where: { | ||
| emailAccountId, | ||
| categoryId: { not: null }, | ||
| category: { | ||
| name: | ||
| FRONTEND_TO_BACKEND_CATEGORY[ | ||
| category as keyof typeof FRONTEND_TO_BACKEND_CATEGORY | ||
| ] || undefined, | ||
| }, | ||
| }, | ||
| select: { email: true }, | ||
| }); | ||
|
|
||
| // Handle "Other" category - get all senders not in the main categories | ||
| if (category === "Other") { | ||
| const otherSenders = await prisma.newsletter.findMany({ | ||
| where: { | ||
| emailAccountId, | ||
| categoryId: { not: null }, | ||
| category: { | ||
| name: { | ||
| notIn: ["Newsletter", "Marketing", "Receipt", "Notification"], | ||
| }, | ||
| }, | ||
| }, | ||
| select: { email: true }, | ||
| }); | ||
| senders.push(...otherSenders); | ||
| } | ||
|
|
||
| if (senders.length === 0) { | ||
| throw new SafeError(`No senders found in ${category} category`); | ||
| } | ||
|
|
||
| logger.info("Found senders to archive", { | ||
| category, | ||
| senderCount: senders.length, | ||
| }); | ||
|
|
||
| // Generate unique operation ID | ||
| const operationId = `archive-${category}-${Date.now()}`; | ||
|
|
||
| // Queue the archive operation to run in the background | ||
| await publishArchiveCategoryQueue({ | ||
| emailAccountId, | ||
| operationId, | ||
| category, | ||
| senders: senders.map((s) => s.email), | ||
| }); | ||
|
|
||
| logger.info("Queued archive operation", { | ||
| emailAccountId, | ||
| operationId, | ||
| category, | ||
| senderCount: senders.length, | ||
| }); | ||
|
|
||
| return { | ||
| success: true, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix category filtering for custom categories
FRONTEND_TO_BACKEND_CATEGORY[...] returns undefined for any custom category. Prisma drops name: undefined, so the findMany falls back to “any categoryId !== null”. Triggering archive on a custom category would queue all categorized senders for archiving (same bug exists in markCategoryAsReadAction). Please branch the filter: use the mapped backend name when available, handle "Other" separately, and fall back to category.name = category for user-defined categories.
- const senders = await prisma.newsletter.findMany({
- where: {
- emailAccountId,
- categoryId: { not: null },
- category: {
- name:
- FRONTEND_TO_BACKEND_CATEGORY[
- category as keyof typeof FRONTEND_TO_BACKEND_CATEGORY
- ] || undefined,
- },
- },
- select: { email: true },
- });
+ const backendName =
+ FRONTEND_TO_BACKEND_CATEGORY[
+ category as keyof typeof FRONTEND_TO_BACKEND_CATEGORY
+ ];
+ const senders = await prisma.newsletter.findMany({
+ where: {
+ emailAccountId,
+ categoryId: { not: null },
+ ...(category === "Other"
+ ? {}
+ : backendName === null
+ ? {
+ category: {
+ name: {
+ notIn: ["Newsletter", "Marketing", "Receipt", "Notification"],
+ },
+ },
+ }
+ : {
+ category: {
+ name: backendName ?? category,
+ },
+ }),
+ },
+ select: { email: true },
+ });Apply the same guard in markCategoryAsReadAction to keep its scope correct.
As per coding guidelines
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In apps/web/utils/actions/deep-clean.ts around lines 40 to 100 the category
filter uses FRONTEND_TO_BACKEND_CATEGORY[...] which returns undefined for custom
categories causing Prisma to ignore the name filter and match all categorized
senders; change the logic to branch the filter: if
FRONTEND_TO_BACKEND_CATEGORY[category] exists use category.name = mappedName,
else if category === "Other" keep the existing "notIn" logic, otherwise set
category.name = category (the user-defined category string); apply the identical
guard/fallback change in markCategoryAsReadAction so user-defined categories are
filtered correctly and do not match all categorized senders.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
apps/web/utils/categorize/senders/categorize.ts(4 hunks)
🧰 Additional context used
📓 Path-based instructions (8)
apps/web/**/*.{ts,tsx}
📄 CodeRabbit inference engine (apps/web/CLAUDE.md)
apps/web/**/*.{ts,tsx}: Use TypeScript with strict null checks
Path aliases: Use@/for imports from project root
Use proper error handling with try/catch blocks
Format code with Prettier
Leverage TypeScript inference for better DX
Files:
apps/web/utils/categorize/senders/categorize.ts
!{.cursor/rules/*.mdc}
📄 CodeRabbit inference engine (.cursor/rules/cursor-rules.mdc)
Never place rule files in the project root, in subdirectories outside .cursor/rules, or in any other location
Files:
apps/web/utils/categorize/senders/categorize.ts
**/*.ts
📄 CodeRabbit inference engine (.cursor/rules/form-handling.mdc)
**/*.ts: The same validation should be done in the server action too
Define validation schemas using Zod
Files:
apps/web/utils/categorize/senders/categorize.ts
**/*.{ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/logging.mdc)
**/*.{ts,tsx}: UsecreateScopedLoggerfor logging in backend TypeScript files
Typically add the logger initialization at the top of the file when usingcreateScopedLogger
Only use.with()on a logger instance within a specific function, not for a global loggerImport Prisma in the project using
import prisma from "@/utils/prisma";
**/*.{ts,tsx}: Don't use TypeScript enums.
Don't use TypeScript const enum.
Don't use the TypeScript directive @ts-ignore.
Don't use primitive type aliases or misleading types.
Don't use empty type parameters in type aliases and interfaces.
Don't use any or unknown as type constraints.
Don't use implicit any type on variable declarations.
Don't let variables evolve into any type through reassignments.
Don't use non-null assertions with the ! postfix operator.
Don't misuse the non-null assertion operator (!) in TypeScript files.
Don't use user-defined types.
Use as const instead of literal types and type annotations.
Use export type for types.
Use import type for types.
Don't declare empty interfaces.
Don't merge interfaces and classes unsafely.
Don't use overload signatures that aren't next to each other.
Use the namespace keyword instead of the module keyword to declare TypeScript namespaces.
Don't use TypeScript namespaces.
Don't export imported variables.
Don't add type annotations to variables, parameters, and class properties that are initialized with literal expressions.
Don't use parameter properties in class constructors.
Use either T[] or Array consistently.
Initialize each enum member value explicitly.
Make sure all enum members are literal values.
Files:
apps/web/utils/categorize/senders/categorize.ts
apps/web/utils/**
📄 CodeRabbit inference engine (.cursor/rules/project-structure.mdc)
Create utility functions in
utils/folder for reusable logic
Files:
apps/web/utils/categorize/senders/categorize.ts
apps/web/utils/**/*.ts
📄 CodeRabbit inference engine (.cursor/rules/project-structure.mdc)
apps/web/utils/**/*.ts: Use lodash utilities for common operations (arrays, objects, strings)
Import specific lodash functions to minimize bundle size
Files:
apps/web/utils/categorize/senders/categorize.ts
**/*.{js,jsx,ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/ultracite.mdc)
**/*.{js,jsx,ts,tsx}: Don't useelements in Next.js projects.
Don't use elements in Next.js projects.
Don't use namespace imports.
Don't access namespace imports dynamically.
Don't use global eval().
Don't use console.
Don't use debugger.
Don't use var.
Don't use with statements in non-strict contexts.
Don't use the arguments object.
Don't use consecutive spaces in regular expression literals.
Don't use the comma operator.
Don't use unnecessary boolean casts.
Don't use unnecessary callbacks with flatMap.
Use for...of statements instead of Array.forEach.
Don't create classes that only have static members (like a static namespace).
Don't use this and super in static contexts.
Don't use unnecessary catch clauses.
Don't use unnecessary constructors.
Don't use unnecessary continue statements.
Don't export empty modules that don't change anything.
Don't use unnecessary escape sequences in regular expression literals.
Don't use unnecessary labels.
Don't use unnecessary nested block statements.
Don't rename imports, exports, and destructured assignments to the same name.
Don't use unnecessary string or template literal concatenation.
Don't use String.raw in template literals when there are no escape sequences.
Don't use useless case statements in switch statements.
Don't use ternary operators when simpler alternatives exist.
Don't use useless this aliasing.
Don't initialize variables to undefined.
Don't use the void operators (they're not familiar).
Use arrow functions instead of function expressions.
Use Date.now() to get milliseconds since the Unix Epoch.
Use .flatMap() instead of map().flat() when possible.
Use literal property access instead of computed property access.
Don't use parseInt() or Number.parseInt() when binary, octal, or hexadecimal literals work.
Use concise optional chaining instead of chained logical expressions.
Use regular expression literals instead of the RegExp constructor when possible.
Don't use number literal object member names th...
Files:
apps/web/utils/categorize/senders/categorize.ts
!pages/_document.{js,jsx,ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/ultracite.mdc)
!pages/_document.{js,jsx,ts,tsx}: Don't import next/document outside of pages/_document.jsx in Next.js projects.
Don't import next/document outside of pages/_document.jsx in Next.js projects.
Files:
apps/web/utils/categorize/senders/categorize.ts
🧬 Code graph analysis (1)
apps/web/utils/categorize/senders/categorize.ts (3)
apps/web/utils/llms/types.ts (1)
EmailAccountWithAI(10-29)apps/web/utils/category.server.ts (1)
getUserCategories(16-25)apps/web/utils/priority.ts (2)
PriorityLevel(1-1)priorityToNumber(9-11)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: cubic · AI code reviewer
- GitHub Check: test
🔇 Additional comments (6)
apps/web/utils/categorize/senders/categorize.ts (6)
12-13: LGTM: Imports follow best practices.The imports correctly use
import typefor types and named imports for utilities.
22-24: LGTM: Consistent user category retrieval.The change from an optional parameter to explicit database retrieval makes the function's behavior clearer and more predictable.
63-69: LGTM: Clean signature extension.The optional
priorityparameter is properly typed and positioned at the end of the parameter list.
71-101: LGTM: Robust category upsert with fallback to defaults.The logic correctly:
- Checks provided categories first
- Falls back to default categories
- Upserts to ensure category exists in DB
- Tracks newly created categories
Note: The empty
update: {}object (line 88) means existing category descriptions won't be updated even if defaults change. This appears intentional to preserve user modifications.
104-119: LGTM: Consistent priority handling.The priority is correctly:
- Converted to a number using
priorityToNumber- Set to
nullwhen not provided- Applied consistently in both update and create paths
44-44: No issues found. Code is properly typed and safe.The
aiResultobject returned fromaiCategorizeSenderis validated by a Zod schema that explicitly definespriorityasz.enum(["low", "medium", "high"]). This ensures the priority field exists and is constrained to valid values. TheupdateSenderCategoryfunction correctly accepts an optionalpriorityparameter of typePriorityLevel, and the code properly handles both defined and undefined values. TypeScript's type system catches any incompatibilities automatically—no explicit type guards are necessary.Likely an incorrect or invalid review comment.
| export async function getCategories() { | ||
| // Use default categories instead of requiring user-created categories | ||
| const defaultCategories = Object.values(defaultCategory) | ||
| .filter((category) => category.enabled) | ||
| .map((category) => ({ | ||
| id: category.name, // Use name as id for default categories | ||
| name: category.name, | ||
| description: category.description, | ||
| })); | ||
|
|
||
| return { categories: defaultCategories }; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Verify behavioral change: getCategories now returns defaults only.
This is a significant change:
- Function no longer fetches user-specific categories from the database
- Always returns default categories filtered by
enabled - Uses
category.nameasid(line 169), which may cause type confusion if consumers expect actual database IDs
The AI summary indicates this is intentional, but verify that:
- All callers are updated to expect default categories
- Downstream code can handle name-as-id pattern
- Code that needs user-specific categories uses
getUserCategoriesinstead
🏁 Script executed:
#!/bin/bash
# Description: Find all usages of getCategories to verify they expect default categories
# Search for getCategories function calls
rg -n "getCategories\(" --type ts -C 3
# Also check for any imports of this function
rg -n "import.*getCategories" --type tsLength of output: 2370
Now I need to check the imports and verify which getCategories is actually being used in handle-batch.ts and any other files:Now let me search for imports and check which version of the function is being used:
---
🏁 Script executed:
```shell
#!/bin/bash
# Find imports of getCategories in handle-batch.ts
grep -n "import.*getCategories\|from.*categorize" apps/web/app/api/user/categorize/senders/batch/handle-batch.ts
# Also show the actual import statements context
grep -B 5 -A 5 "getCategories" apps/web/app/api/user/categorize/senders/batch/handle-batch.ts | head -20
# Check category type definition
fd -e ts -e tsx | xargs grep -l "type.*Category\|interface.*Category" | grep -v node_modules | head -5
# Check the structure of returned data from getCategories in categorize.ts
cat apps/web/utils/categorize/senders/categorize.ts | grep -A 30 "export async function getCategories"
Length of output: 2192
Let me check the Category type definition and verify the type compatibility:
🏁 Script executed:
#!/bin/bash
# Find Category type definition
rg -n "type Category|interface Category" --type ts -A 5
# Check what categorizeWithAi does with the categories parameter
rg -n "preCategorizeSendersWithStaticRules\|categories" apps/web/utils/categorize/senders/categorize.ts -A 2 -B 2 | head -30
# Check the full signature of categorizeWithAi
rg -n "export async function categorizeWithAi" --type ts -A 10Length of output: 5680
Let me check how the categories variable is used in handle-batch.ts after it's extracted:
🏁 Script executed:
#!/bin/bash
# Show the full context of how categories is used after getCategories() call
sed -n '44,100p' apps/web/app/api/user/categorize/senders/batch/handle-batch.ts
# Also check if there's any type assertion or validation
rg -n "categories" apps/web/app/api/user/categorize/senders/batch/handle-batch.ts -B 2 -A 2Length of output: 2251
Let me check the updateSenderCategory function signature:
🏁 Script executed:
#!/bin/bash
# Find updateSenderCategory function definition and its categories parameter type
rg -n "export.*function updateSenderCategory|function updateSenderCategory" --type ts -A 12
# Also check for any type issues or validation in handle-batch around categories
rg -n "updateSenderCategory" apps/web/app/api/user/categorize/senders/batch/handle-batch.ts -B 3 -A 3Length of output: 1436
Let me check how updateSenderCategory uses the categories parameter and the id field:
🏁 Script executed:
#!/bin/bash
# Show full implementation of updateSenderCategory
sed -n '58,90p' apps/web/utils/categorize/senders/categorize.ts
# Search for how id is used in the function
rg -n "categories\[|categories\.id|\.id" apps/web/utils/categorize/senders/categorize.ts -B 2 -A 2 | head -40Length of output: 1688
Now let me check what defaultCategory contains to understand the full scope:
🏁 Script executed:
#!/bin/bash
# Find defaultCategory definition
rg -n "const defaultCategory|export.*defaultCategory" --type ts -B 2 -A 10 | head -50
# Also check if there's a type definition that shows what id should be
rg -n "defaultCategory" apps/web/utils/categorize/senders/categorize.ts -B 2 -A 2Length of output: 3374
ID field uses category name instead of database ID, breaking updateSenderCategory functionality.
The change is intentional per the AI summary, but verification reveals a critical issue:
getCategories()returns default categories withid: category.name(string like "Newsletter", "Unknown")updateSenderCategory()expectsidto be an actual database primary key (UUID)- Lines 110 and 116 of
updateSenderCategory()savecategoryId: category.iddirectly to the database - This stores category names as foreign keys instead of proper category IDs, breaking the sender-category relationship
- Line 98's comparison (
c.id === upsertedCategory.id) will fail when matching defaults against database records
Required fix: getCategories() should either:
- Generate proper UUIDs for default categories (consistent with database IDs), or
- Return categories without an
idfield (matchingcategorizeWithAiexpectations), and letupdateSenderCategory()handle category lookup differently
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
🧹 Nitpick comments (10)
apps/web/app/api/user/categorize/senders/batch/handle-batch.ts (1)
111-134: Consider removing commented code.The large block of commented code should either be implemented or removed. Keeping commented code in the codebase reduces readability and can cause confusion about whether the functionality is needed.
If this functionality is planned for future implementation, consider:
- Opening a separate issue/task to track it
- Removing the commented code (version control preserves the history)
- Referencing the issue in a TODO comment if needed
If the functionality is not needed, simply remove the commented lines.
apps/web/__tests__/ai-categorize-senders.test.ts (1)
159-159: Add a light check for priority shape (optional)Since results now include priority, consider asserting it’s one of "low" | "medium" | "high" to catch schema drift.
- expect(result?.category).toBe("Unknown"); + expect(result?.category).toBe("Unknown"); + // optional: basic shape check + expect(["low","medium","high"]).toContain(result?.priority);apps/web/utils/ai/categorize-sender/ai-categorize-single-sender.ts (3)
55-63: Guard categories against unexpected values; fallback to "Unknown"The previous implementation rejected unknown categories; now any string passes. Add an allowlist based on provided categories to avoid downstream breakage.
const aiResponse = await generateObject({ ...modelOptions, system: CATEGORIZE_SENDER_SYSTEM_PROMPT, prompt, schema: senderCategorizationSchema, }); - - return aiResponse.object; + const allowed = new Set([...categories.map((c) => c.name), "Unknown"]); + const obj = aiResponse.object; + const safeCategory = allowed.has(obj.category) ? obj.category : "Unknown"; + return { ...obj, category: safeCategory };Please confirm that all consumers can handle only-known categories plus "Unknown".
23-45: Trim and sanitize email content to control tokens (optional)Cap subject/snippet length and normalize whitespace to reduce model tokens and flakiness.
+const truncate = (s: string, n = 200) => s.replace(/\s+/g, " ").slice(0, n); ... .map( (email) => - `<email><subject>${email.subject}</subject><snippet>${email.snippet}</snippet></email>`, + `<email><subject>${truncate(email.subject)}</subject><snippet>${truncate(email.snippet)}</snippet></email>`, )
1-10: Add scoped logging for observability (optional)Per guidelines, initialize a scoped logger and log counts (not PII) and output category/priority.
import { createGenerateObject } from "@/utils/llms"; +// import { createScopedLogger } from "@/utils/logger"; // adjust path per project +// const log = createScopedLogger("ai.categorize.single");And after returning, consider logging: log.debug("categorized", { sender, category: safeCategory, priority: obj.priority });
apps/web/utils/ai/categorize-sender/ai-categorize-senders.ts (5)
69-73: Instruction block integration — good; also consider batching for large inputsInstructions now centralized — nice. For many senders, consider batching (e.g., chunks of 25–50) to avoid token/latency spikes.
I can draft a chunking helper and loop to merge results if helpful.
39-60: Limit per-sender emails and sanitize to reduce tokens (recommended)Keep only a few recent emails and trim fields.
- ? `<recent_emails> - ${emails + ? `<recent_emails> + ${emails.slice(0, 3) .map( (s) => ` <email> - <subject>${s.subject}</subject> - <snippet>${s.snippet}</snippet> + <subject>${s.subject?.replace(/\s+/g, " ").slice(0, 200)}</subject> + <snippet>${s.snippet?.replace(/\s+/g, " ").slice(0, 200)}</snippet> </email>`, ) .join("")} </recent_emails>`
94-95: Reintroduce category allowlist to prevent unexpected valuesReturn only categories in the provided list or "Unknown" to keep downstream logic stable.
- return matchedSenders; + const allowed = new Set([...categories.map((c) => c.name), "Unknown"]); + return matchedSenders.map((s) => ({ + ...s, + category: allowed.has(s.category ?? "") ? s.category : "Unknown", + }));Confirm no consumer expects arbitrary new categories beyond the configured set.
101-122: Include rationale if useful for UI/debug (optional)You capture rationale in the schema but discard it here. Returning it can aid UX/tooltips and debugging.
- return { sender, category: r.category, priority: r.priority }; + return { sender, category: r.category, priority: r.priority, rationale: r.rationale };
74-81: Add scoped logging (optional)Initialize a scoped logger and log batch size and result summary (counts by category/priority), avoiding PII.
Example:
- log.debug("bulk_categorize:start", { count: senders.length });
- log.debug("bulk_categorize:done", { matched: matchedSenders.length });
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
apps/web/__tests__/ai-categorize-senders.test.ts(3 hunks)apps/web/app/api/user/categorize/senders/batch/handle-batch.ts(2 hunks)apps/web/utils/ai/categorize-sender/ai-categorize-senders.ts(6 hunks)apps/web/utils/ai/categorize-sender/ai-categorize-single-sender.ts(3 hunks)apps/web/utils/ai/categorize-sender/prompts.ts(1 hunks)
🧰 Additional context used
📓 Path-based instructions (18)
apps/web/**/*.{ts,tsx}
📄 CodeRabbit inference engine (apps/web/CLAUDE.md)
apps/web/**/*.{ts,tsx}: Use TypeScript with strict null checks
Path aliases: Use@/for imports from project root
Use proper error handling with try/catch blocks
Format code with Prettier
Leverage TypeScript inference for better DX
Files:
apps/web/utils/ai/categorize-sender/ai-categorize-single-sender.tsapps/web/__tests__/ai-categorize-senders.test.tsapps/web/app/api/user/categorize/senders/batch/handle-batch.tsapps/web/utils/ai/categorize-sender/prompts.tsapps/web/utils/ai/categorize-sender/ai-categorize-senders.ts
!{.cursor/rules/*.mdc}
📄 CodeRabbit inference engine (.cursor/rules/cursor-rules.mdc)
Never place rule files in the project root, in subdirectories outside .cursor/rules, or in any other location
Files:
apps/web/utils/ai/categorize-sender/ai-categorize-single-sender.tsapps/web/__tests__/ai-categorize-senders.test.tsapps/web/app/api/user/categorize/senders/batch/handle-batch.tsapps/web/utils/ai/categorize-sender/prompts.tsapps/web/utils/ai/categorize-sender/ai-categorize-senders.ts
**/*.ts
📄 CodeRabbit inference engine (.cursor/rules/form-handling.mdc)
**/*.ts: The same validation should be done in the server action too
Define validation schemas using Zod
Files:
apps/web/utils/ai/categorize-sender/ai-categorize-single-sender.tsapps/web/__tests__/ai-categorize-senders.test.tsapps/web/app/api/user/categorize/senders/batch/handle-batch.tsapps/web/utils/ai/categorize-sender/prompts.tsapps/web/utils/ai/categorize-sender/ai-categorize-senders.ts
**/*.{ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/logging.mdc)
**/*.{ts,tsx}: UsecreateScopedLoggerfor logging in backend TypeScript files
Typically add the logger initialization at the top of the file when usingcreateScopedLogger
Only use.with()on a logger instance within a specific function, not for a global loggerImport Prisma in the project using
import prisma from "@/utils/prisma";
**/*.{ts,tsx}: Don't use TypeScript enums.
Don't use TypeScript const enum.
Don't use the TypeScript directive @ts-ignore.
Don't use primitive type aliases or misleading types.
Don't use empty type parameters in type aliases and interfaces.
Don't use any or unknown as type constraints.
Don't use implicit any type on variable declarations.
Don't let variables evolve into any type through reassignments.
Don't use non-null assertions with the ! postfix operator.
Don't misuse the non-null assertion operator (!) in TypeScript files.
Don't use user-defined types.
Use as const instead of literal types and type annotations.
Use export type for types.
Use import type for types.
Don't declare empty interfaces.
Don't merge interfaces and classes unsafely.
Don't use overload signatures that aren't next to each other.
Use the namespace keyword instead of the module keyword to declare TypeScript namespaces.
Don't use TypeScript namespaces.
Don't export imported variables.
Don't add type annotations to variables, parameters, and class properties that are initialized with literal expressions.
Don't use parameter properties in class constructors.
Use either T[] or Array consistently.
Initialize each enum member value explicitly.
Make sure all enum members are literal values.
Files:
apps/web/utils/ai/categorize-sender/ai-categorize-single-sender.tsapps/web/__tests__/ai-categorize-senders.test.tsapps/web/app/api/user/categorize/senders/batch/handle-batch.tsapps/web/utils/ai/categorize-sender/prompts.tsapps/web/utils/ai/categorize-sender/ai-categorize-senders.ts
apps/web/utils/**
📄 CodeRabbit inference engine (.cursor/rules/project-structure.mdc)
Create utility functions in
utils/folder for reusable logic
Files:
apps/web/utils/ai/categorize-sender/ai-categorize-single-sender.tsapps/web/utils/ai/categorize-sender/prompts.tsapps/web/utils/ai/categorize-sender/ai-categorize-senders.ts
apps/web/utils/**/*.ts
📄 CodeRabbit inference engine (.cursor/rules/project-structure.mdc)
apps/web/utils/**/*.ts: Use lodash utilities for common operations (arrays, objects, strings)
Import specific lodash functions to minimize bundle size
Files:
apps/web/utils/ai/categorize-sender/ai-categorize-single-sender.tsapps/web/utils/ai/categorize-sender/prompts.tsapps/web/utils/ai/categorize-sender/ai-categorize-senders.ts
**/*.{js,jsx,ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/ultracite.mdc)
**/*.{js,jsx,ts,tsx}: Don't useelements in Next.js projects.
Don't use elements in Next.js projects.
Don't use namespace imports.
Don't access namespace imports dynamically.
Don't use global eval().
Don't use console.
Don't use debugger.
Don't use var.
Don't use with statements in non-strict contexts.
Don't use the arguments object.
Don't use consecutive spaces in regular expression literals.
Don't use the comma operator.
Don't use unnecessary boolean casts.
Don't use unnecessary callbacks with flatMap.
Use for...of statements instead of Array.forEach.
Don't create classes that only have static members (like a static namespace).
Don't use this and super in static contexts.
Don't use unnecessary catch clauses.
Don't use unnecessary constructors.
Don't use unnecessary continue statements.
Don't export empty modules that don't change anything.
Don't use unnecessary escape sequences in regular expression literals.
Don't use unnecessary labels.
Don't use unnecessary nested block statements.
Don't rename imports, exports, and destructured assignments to the same name.
Don't use unnecessary string or template literal concatenation.
Don't use String.raw in template literals when there are no escape sequences.
Don't use useless case statements in switch statements.
Don't use ternary operators when simpler alternatives exist.
Don't use useless this aliasing.
Don't initialize variables to undefined.
Don't use the void operators (they're not familiar).
Use arrow functions instead of function expressions.
Use Date.now() to get milliseconds since the Unix Epoch.
Use .flatMap() instead of map().flat() when possible.
Use literal property access instead of computed property access.
Don't use parseInt() or Number.parseInt() when binary, octal, or hexadecimal literals work.
Use concise optional chaining instead of chained logical expressions.
Use regular expression literals instead of the RegExp constructor when possible.
Don't use number literal object member names th...
Files:
apps/web/utils/ai/categorize-sender/ai-categorize-single-sender.tsapps/web/__tests__/ai-categorize-senders.test.tsapps/web/app/api/user/categorize/senders/batch/handle-batch.tsapps/web/utils/ai/categorize-sender/prompts.tsapps/web/utils/ai/categorize-sender/ai-categorize-senders.ts
!pages/_document.{js,jsx,ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/ultracite.mdc)
!pages/_document.{js,jsx,ts,tsx}: Don't import next/document outside of pages/_document.jsx in Next.js projects.
Don't import next/document outside of pages/_document.jsx in Next.js projects.
Files:
apps/web/utils/ai/categorize-sender/ai-categorize-single-sender.tsapps/web/__tests__/ai-categorize-senders.test.tsapps/web/app/api/user/categorize/senders/batch/handle-batch.tsapps/web/utils/ai/categorize-sender/prompts.tsapps/web/utils/ai/categorize-sender/ai-categorize-senders.ts
apps/web/utils/ai/**/*.{ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/llm.mdc)
apps/web/utils/ai/**/*.{ts,tsx}: Place main LLM feature implementations under apps/web/utils/ai/
LLM feature functions should follow the provided TypeScript pattern (separate system/user prompts, use createGenerateObject, Zod schema validation, early validation, return result.object)
Keep system prompts and user prompts separate
System prompt should define the LLM's role and task specifications
User prompt should contain the actual data and context
Always define a Zod schema for response validation
Make Zod schemas as specific as possible to guide LLM output
Use descriptive scoped loggers for each feature
Log inputs and outputs with appropriate log levels and include relevant context
Implement early returns for invalid inputs
Use proper error types and logging for failures
Implement fallbacks for AI failures
Add retry logic for transient failures using withRetry
Use XML-like tags to structure data in prompts
Remove excessive whitespace and truncate long inputs in prompts
Format prompt data consistently across similar functions
Use TypeScript types for all parameters and return values in LLM features
Define clear interfaces for complex input/output structures in LLM features
Files:
apps/web/utils/ai/categorize-sender/ai-categorize-single-sender.tsapps/web/utils/ai/categorize-sender/prompts.tsapps/web/utils/ai/categorize-sender/ai-categorize-senders.ts
apps/web/utils/{ai,llms}/**/*.{ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/llm.mdc)
Keep related AI functions co-located and extract common patterns into utilities; document complex AI logic with clear comments
Files:
apps/web/utils/ai/categorize-sender/ai-categorize-single-sender.tsapps/web/utils/ai/categorize-sender/prompts.tsapps/web/utils/ai/categorize-sender/ai-categorize-senders.ts
**/*.test.{ts,js}
📄 CodeRabbit inference engine (.cursor/rules/security.mdc)
Include security tests in your test suites to verify authentication, authorization, and error handling.
Files:
apps/web/__tests__/ai-categorize-senders.test.ts
**/*.{test,spec}.{js,jsx,ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/ultracite.mdc)
**/*.{test,spec}.{js,jsx,ts,tsx}: Don't use export or module.exports in test files.
Don't use focused tests.
Don't use disabled tests.
Make sure the assertion function, like expect, is placed inside an it() function call.
Don't nest describe() blocks too deeply in test files.
Don't use focused tests.
Don't use disabled tests.
Don't use export or module.exports in test files.
Files:
apps/web/__tests__/ai-categorize-senders.test.ts
apps/web/__tests__/**/*.{ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/llm.mdc)
Place LLM-specific tests under apps/web/tests/
Files:
apps/web/__tests__/ai-categorize-senders.test.ts
**/*.test.{ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/testing.mdc)
**/*.test.{ts,tsx}: Use Vitest (vitest) as the testing framework
Colocate tests next to the file under test (e.g., dir/format.ts with dir/format.test.ts)
In tests, mock theserver-onlymodule withvi.mock("server-only", () => ({}));
When testing code that uses Prisma, mock it withvi.mock("@/utils/prisma")and use the mock from@/utils/__mocks__/prisma
Use provided helpers for mocks: import{ getEmail, getEmailAccount, getRule }from@/__tests__/helpers
Each test should be independent
Use descriptive test names
Mock external dependencies in tests
Clean up mocks between tests (e.g.,vi.clearAllMocks()inbeforeEach)
Avoid testing implementation details; focus on observable behavior
Do not mock the Logger
Files:
apps/web/__tests__/ai-categorize-senders.test.ts
**/__tests__/**
📄 CodeRabbit inference engine (.cursor/rules/testing.mdc)
Place AI tests in the
__tests__directory and exclude them from the default test run (they use a real LLM)
Files:
apps/web/__tests__/ai-categorize-senders.test.ts
apps/web/__tests__/**/*.test.ts
📄 CodeRabbit inference engine (.cursor/rules/llm-test.mdc)
apps/web/__tests__/**/*.test.ts: Place all LLM-related tests under apps/web/tests/
Use Vitest in LLM tests and import { describe, expect, test, vi, beforeEach } from "vitest"
Mock the Next.js server runtime marker by adding vi.mock("server-only", () => ({})) in LLM tests
Gate LLM tests behind RUN_AI_TESTS using describe.runIf(process.env.RUN_AI_TESTS === "true")
Call vi.clearAllMocks() in a beforeEach for LLM tests
Set a TIMEOUT of 15_000ms for LLM-related tests and pass it to long-running tests/describe blocks
Create helper functions for common test data (e.g., getUser, getTestData) to reduce duplication
Include standard test cases: happy path, error handling, edge cases (empty/null), different user configurations, and various input formats
Use console.debug to log generated LLM content for inspection (e.g., console.debug("Generated content:\n", result.content))
Do not mock the actual LLM call in these tests; exercise real LLM integrations
Test both AI and non-AI paths, including cases where no AI processing is required
Prefer existing helpers from @/tests/helpers.ts (getEmailAccount, getEmail, getRule, getMockMessage, getMockExecutedRule) over custom helpers
Files:
apps/web/__tests__/ai-categorize-senders.test.ts
apps/web/app/**
📄 CodeRabbit inference engine (apps/web/CLAUDE.md)
NextJS app router structure with (app) directory
Files:
apps/web/app/api/user/categorize/senders/batch/handle-batch.ts
apps/web/app/api/**/*.{ts,js}
📄 CodeRabbit inference engine (.cursor/rules/security-audit.mdc)
apps/web/app/api/**/*.{ts,js}: All API route handlers in 'apps/web/app/api/' must use authentication middleware: withAuth, withEmailAccount, or withError (with custom authentication logic).
All Prisma queries in API routes must include user/account filtering (e.g., emailAccountId or userId in WHERE clauses) to prevent unauthorized data access.
All parameters used in API routes must be validated before use; do not use parameters from 'params' or request bodies directly in queries without validation.
Request bodies in API routes should use Zod schemas for validation.
API routes should only return necessary fields using Prisma's 'select' and must not include sensitive data in error messages.
Error messages in API routes must not reveal internal details; use generic errors and SafeError for user-facing errors.
All QStash endpoints (API routes called via publishToQstash or publishToQstashQueue) must use verifySignatureAppRouter to verify request authenticity.
All cron endpoints in API routes must use hasCronSecret or hasPostCronSecret for authentication.
Do not hardcode weak or plaintext secrets in API route files; secrets must not be directly assigned as string literals.
Review all new withError usage in API routes to ensure custom authentication is implemented where required.
Files:
apps/web/app/api/user/categorize/senders/batch/handle-batch.ts
🧬 Code graph analysis (3)
apps/web/utils/ai/categorize-sender/ai-categorize-single-sender.ts (1)
apps/web/utils/ai/categorize-sender/prompts.ts (3)
CATEGORIZATION_INSTRUCTIONS(7-29)CATEGORIZE_SENDER_SYSTEM_PROMPT(3-5)senderCategorizationSchema(31-39)
apps/web/app/api/user/categorize/senders/batch/handle-batch.ts (1)
apps/web/utils/categorize/senders/categorize.ts (1)
getCategories(164-175)
apps/web/utils/ai/categorize-sender/ai-categorize-senders.ts (1)
apps/web/utils/ai/categorize-sender/prompts.ts (3)
bulkSenderCategorizationItemSchema(41-44)CATEGORIZATION_INSTRUCTIONS(7-29)CATEGORIZE_SENDER_SYSTEM_PROMPT(3-5)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: cubic · AI code reviewer
- GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (8)
apps/web/app/api/user/categorize/senders/batch/handle-batch.ts (1)
107-107: Priority field implementation looks good.The conditional check for the priority field appropriately handles cases where priority may or may not be present in the categorization result. This defensive approach ensures backward compatibility.
Minor note: If the TypeScript type for
resultincludespriorityas an optional property (e.g.,priority?: string), you could simplify to:priority: result.priority,However, the current explicit check is also perfectly acceptable and more defensive.
apps/web/__tests__/ai-categorize-senders.test.ts (2)
2-2: Good import/path usageImport aligns with the new centralized bulk API and respects the "@/” alias. LGTM.
135-135: Assert Unknown via direct equality — correctSwitching from containment to direct equality matches the new contract. LGTM.
apps/web/utils/ai/categorize-sender/ai-categorize-single-sender.ts (1)
47-47: Economy model switch — verify quality/latency trade-offConfirm categorization accuracy is acceptable on "economy" for real user data; otherwise gate by feature flag or user plan.
Would you like me to add a quick A/B verification harness comparing "default" vs "economy" on a sampled sender set?
apps/web/utils/ai/categorize-sender/ai-categorize-senders.ts (2)
9-13: Centralized prompts/schemas — good consolidationImporting shared system/instructions/schema keeps flows consistent. LGTM.
16-17: Schema: align with bulk item shape — goodValidates LLM output at the array item level. LGTM.
apps/web/utils/ai/categorize-sender/prompts.ts (2)
3-30: Prompts centralization and priority guidance — solidClear system/instructions; priority enum in schema is helpful. LGTM.
31-44: Remove this review comment—zod 3.25.46 is a valid, stable releaseThe codebase uses zod 3.25.46 across all packages, which is an official, valid release. No v8.x or invalid versions exist in package.json. The schema definitions in prompts.ts require no version-related fixes.
Likely an incorrect or invalid review comment.
| const categoriesResult = await getCategories(); | ||
| const { categories } = categoriesResult; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Verify the removal of user-specific category support.
The change from getCategories({ emailAccountId }) to getCategories() removes user-specific categories in favor of default categories. This is a significant behavioral change that affects users who may have created custom categories.
Please confirm:
- Is this breaking change intentional and documented?
- Have existing users with custom categories been migrated or notified?
- Are there plans to support user-specific categories in the future?
Run this script to check if any users have custom categories that would be affected:
🏁 Script executed:
#!/bin/bash
# Check if there are existing user-created categories in the database
ast-grep --pattern $'prisma.category.findMany({
$$$
})'Length of output: 1042
---
🏁 Script executed:
```shell
#!/bin/bash
# Find the getCategories function definition and its implementation
rg -n "export.*function.*getCategories|const getCategories|export const getCategories" --type ts --type tsx
Length of output: 27
🏁 Script executed:
#!/bin/bash
# Get the full implementation of getCategories function
ast-grep --pattern $'function getCategories($_) {
$$$
}'
# Also try const version
ast-grep --pattern $'const getCategories = $_'Length of output: 340
🏁 Script executed:
#!/bin/bash
# Check the imports in the handle-batch.ts file to see where getCategories comes from
rg -n "getCategories" apps/web/app/api/user/categorize/senders/batch/handle-batch.ts -B5 -A5Length of output: 921
---
🏁 Script executed:
```shell
#!/bin/bash
# Find the getCategories function in the categorize utility file
rg -n "getCategories" apps/web/utils/categorize/senders/categorize.ts -B2 -A15
Length of output: 585
🏁 Script executed:
#!/bin/bash
# Also check if there's a categorize folder with index file
fd -t f "categorize" apps/web/utils/categorize/ | head -20Length of output: 47
🏁 Script executed:
#!/bin/bash
# Look for where the function is defined - it might be in a related file
rg -n "export.*getCategories" apps/web/utils/ -B2 -A10Length of output: 1108
---
🏁 Script executed:
```shell
#!/bin/bash
# Check for other usages of getCategories to see scope of impact
rg -n "getCategories\(" apps/web/ --type ts -B2 -A2
Length of output: 1804
🏁 Script executed:
#!/bin/bash
# Get the full context of lines 46-47 and surrounding code in handle-batch.ts
rg -n "." apps/web/app/api/user/categorize/senders/batch/handle-batch.ts | head -120 | tail -80Length of output: 2739
🏁 Script executed:
#!/bin/bash
# Check line 105 and 107 for the issues mentioned in review (magic string and priority)
rg -n "." apps/web/app/api/user/categorize/senders/batch/handle-batch.ts | sed -n '100,140p'Length of output: 939
Remove hardcoded category fallback string and commented code block.
Line 105 uses magic string "Unknown" instead of a constant. The constant UNKNOWN_CATEGORY was removed in this change—add it back or define a replacement constant for maintainability.
Lines 111-134 contain a large commented code block for uncategorized sender logic. Remove this dead code.
Additionally, note that getCategories() at line 46 now returns only default categories, removing user-specific category support. This is a breaking change intentionally reflected in the function implementation (see apps/web/utils/categorize/senders/categorize.ts:165 comment). Ensure users with existing custom categories are properly handled.
🤖 Prompt for AI Agents
In apps/web/app/api/user/categorize/senders/batch/handle-batch.ts around lines
46–47 (and referencing usages at ~105 and the commented block at 111–134):
restore a non-hardcoded UNKNOWN_CATEGORY constant and remove the large
commented-out uncategorized sender logic; specifically, reintroduce a top-level
constant (e.g., UNKNOWN_CATEGORY = "Unknown") and replace the magic string at
line 105 with that constant, delete lines 111–134 entirely, and update the
getCategories() usage so it merges default categories with any existing
user-specific categories (or explicitly fetch user categories and
de-duplicate/append them) before using them — if no user categories exist, fall
back to defaults. Ensure the merged result preserves user custom categories to
avoid the breaking change from returning only defaults.
| sender: result.sender, | ||
| categories, | ||
| categoryName: result.category ?? UNKNOWN_CATEGORY, | ||
| categoryName: result.category ?? "Unknown", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
Replace magic string with a constant.
The hardcoded "Unknown" string should be replaced with a constant for better maintainability and consistency. The previous code used UNKNOWN_CATEGORY which was more maintainable.
Consider defining and using a constant:
+const UNKNOWN_CATEGORY = "Unknown";
+
async function handleBatchInternal(request: Request) {
// ...
for (const result of results) {
await updateSenderCategory({
sender: result.sender,
categories,
- categoryName: result.category ?? "Unknown",
+ categoryName: result.category ?? UNKNOWN_CATEGORY,
emailAccountId,
priority: "priority" in result ? result.priority : undefined,
});
}Alternatively, if UNKNOWN_CATEGORY is defined elsewhere in the codebase (e.g., in apps/web/utils/categorize/senders/categorize.ts), import and use it here.
🤖 Prompt for AI Agents
In apps/web/app/api/user/categorize/senders/batch/handle-batch.ts around line
105, replace the hardcoded "Unknown" magic string with the shared constant
(e.g., UNKNOWN_CATEGORY) to improve maintainability; either import
UNKNOWN_CATEGORY from its module (likely
apps/web/utils/categorize/senders/categorize.ts or the utils index) and use it
in place of "Unknown", or define a local constant if no shared constant exists,
and update the categoryName assignment to reference that constant.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
10 issues found across 28 files
Prompt for AI agents (all 10 issues)
Understand the root cause of the following 10 issues and fix them.
<file name="apps/web/utils/email.ts">
<violation number="1" location="apps/web/utils/email.ts:32">
Returning only the quoted local-part drops the domain for addresses like <"quoted-name"@domain.com>, so callers end up with `quoted-name` instead of the actual email address.</violation>
</file>
<file name="apps/web/utils/actions/deep-clean.ts">
<violation number="1" location="apps/web/utils/actions/deep-clean.ts:49">
Marking the "Other" category as read affects every category because the Prisma filter is stripped when it evaluates to undefined.</violation>
</file>
<file name="apps/web/components/DeepCleanGroupedTable.tsx">
<violation number="1" location="apps/web/components/DeepCleanGroupedTable.tsx:182">
"Mark as Read" actions merely show a toast and never trigger the actual mark-as-read logic.</violation>
<violation number="2" location="apps/web/components/DeepCleanGroupedTable.tsx:378">
Dropping rows where categoryMap lacks an entry causes the entire "Uncategorized" group (which we just created above) to disappear, so senders without a category never render. The table should still render those groups instead of returning null.</violation>
</file>
<file name="apps/web/app/(app)/[emailAccountId]/deep-clean/BulkOperationProgress.tsx">
<violation number="1" location="apps/web/app/(app)/[emailAccountId]/deep-clean/BulkOperationProgress.tsx:35">
Calling setTimeout during render enqueues a new timer every render for each completed operation, leaking timers and hurting performance. Move this timer logic into a useEffect tied to completed operations or remove it.</violation>
</file>
<file name="apps/web/app/api/user/categorize/senders/batch/handle-batch.ts">
<violation number="1" location="apps/web/app/api/user/categorize/senders/batch/handle-batch.ts:46">
Passing default categories that are not real database records means we skip the upsert branch and then write a bogus categoryId (the category name) into `prisma.newsletter.upsert`, which will fail due to the foreign key.</violation>
</file>
<file name="apps/web/utils/redis/bulk-operation-progress.ts">
<violation number="1" location="apps/web/utils/redis/bulk-operation-progress.ts:108">
Auto-completion overwrites explicit failure status, so failed bulk operations get reported as completed.</violation>
</file>
<file name="apps/web/app/(app)/[emailAccountId]/deep-clean/Uncategorized.tsx">
<violation number="1" location="apps/web/app/(app)/[emailAccountId]/deep-clean/Uncategorized.tsx:149">
The auto-categorize switch is controlled by enabled={autoCategorizeSenders}, but the onChange handler only calls setAutoCategorizeAction and never refreshes or updates local state, so the UI snaps back and the user cannot actually change the setting.</violation>
</file>
<file name="apps/web/app/(app)/[emailAccountId]/deep-clean/CategorizeWithAiButton.tsx">
<violation number="1" location="apps/web/app/(app)/[emailAccountId]/deep-clean/CategorizeWithAiButton.tsx:47">
If the categorize action fails we keep isBulkCategorizing stuck at true, so the Deep Clean progress UI never stops. Reset the bulk categorizing flag before throwing to avoid a permanently spinning progress indicator.</violation>
</file>
<file name="apps/web/app/(app)/[emailAccountId]/deep-clean/DeepCleanContent.tsx">
<violation number="1" location="apps/web/app/(app)/[emailAccountId]/deep-clean/DeepCleanContent.tsx:50">
`hasAutoTriggered` is marked true before confirming `emailAccount.id`, so the auto-categorize effect locks itself out whenever the provider is still loading. Move the flag assignment until after the account ID check so the auto-trigger actually fires once the account is available.</violation>
</file>
React with 👍 or 👎 to teach cubic. Mention @cubic-dev-ai to give feedback, ask questions, or re-run the review.
| // Pattern: "quoted-name"@domain.com or 'quoted-name'@domain.com | ||
| const quotedNameMatch = emailPart.match(/^["']([^"']+)["']@/); | ||
| if (quotedNameMatch) { | ||
| return quotedNameMatch[1]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Returning only the quoted local-part drops the domain for addresses like <"quoted-name"@domain.com>, so callers end up with quoted-name instead of the actual email address.
Prompt for AI agents
Address the following comment on apps/web/utils/email.ts at line 32:
<comment>Returning only the quoted local-part drops the domain for addresses like <"quoted-name"@domain.com>, so callers end up with `quoted-name` instead of the actual email address.</comment>
<file context>
@@ -6,13 +6,41 @@ const emailSchema = z.string().email();
+ // Pattern: "quoted-name"@domain.com or 'quoted-name'@domain.com
+ const quotedNameMatch = emailPart.match(/^["']([^"']+)["']@/);
+ if (quotedNameMatch) {
+ return quotedNameMatch[1];
+ }
+
</file context>
| return quotedNameMatch[1]; | |
| return emailPart.replace(/^(["'])([^"']+)\1@/, "$2@"); |
apps/web/utils/actions/deep-clean.ts
Outdated
| name: | ||
| FRONTEND_TO_BACKEND_CATEGORY[ | ||
| category as keyof typeof FRONTEND_TO_BACKEND_CATEGORY | ||
| ] || undefined, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Marking the "Other" category as read affects every category because the Prisma filter is stripped when it evaluates to undefined.
Prompt for AI agents
Address the following comment on apps/web/utils/actions/deep-clean.ts at line 49:
<comment>Marking the "Other" category as read affects every category because the Prisma filter is stripped when it evaluates to undefined.</comment>
<file context>
@@ -0,0 +1,310 @@
+ name:
+ FRONTEND_TO_BACKEND_CATEGORY[
+ category as keyof typeof FRONTEND_TO_BACKEND_CATEGORY
+ ] || undefined,
+ },
+ },
</file context>
✅ Addressed in 9f182e9
| <DropdownMenuItem | ||
| onClick={() => { | ||
| // TODO: Implement mark as read functionality | ||
| toastSuccess({ description: "Marked as read" }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"Mark as Read" actions merely show a toast and never trigger the actual mark-as-read logic.
Prompt for AI agents
Address the following comment on apps/web/components/DeepCleanGroupedTable.tsx at line 182:
<comment>"Mark as Read" actions merely show a toast and never trigger the actual mark-as-read logic.</comment>
<file context>
@@ -0,0 +1,679 @@
+ <DropdownMenuItem
+ onClick={() => {
+ // TODO: Implement mark as read functionality
+ toastSuccess({ description: "Marked as read" });
+ }}
+ >
</file context>
✅ Addressed in 9f182e9
|
|
||
| // Hide completed operations after 5 seconds | ||
| if (isCompleted) { | ||
| setTimeout(() => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Calling setTimeout during render enqueues a new timer every render for each completed operation, leaking timers and hurting performance. Move this timer logic into a useEffect tied to completed operations or remove it.
Prompt for AI agents
Address the following comment on apps/web/app/(app)/[emailAccountId]/deep-clean/BulkOperationProgress.tsx at line 35:
<comment>Calling setTimeout during render enqueues a new timer every render for each completed operation, leaking timers and hurting performance. Move this timer logic into a useEffect tied to completed operations or remove it.</comment>
<file context>
@@ -0,0 +1,63 @@
+
+ // Hide completed operations after 5 seconds
+ if (isCompleted) {
+ setTimeout(() => {
+ // This will be handled by React's re-render when the operation is removed from Redis
+ }, 5000);
</file context>
| const { emailAccount } = userResult; | ||
|
|
||
| const categoriesResult = await getCategories({ emailAccountId }); | ||
| const categoriesResult = await getCategories(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Passing default categories that are not real database records means we skip the upsert branch and then write a bogus categoryId (the category name) into prisma.newsletter.upsert, which will fail due to the foreign key.
Prompt for AI agents
Address the following comment on apps/web/app/api/user/categorize/senders/batch/handle-batch.ts at line 46:
<comment>Passing default categories that are not real database records means we skip the upsert branch and then write a bogus categoryId (the category name) into `prisma.newsletter.upsert`, which will fail due to the foreign key.</comment>
<file context>
@@ -44,7 +43,7 @@ async function handleBatchInternal(request: Request) {
const { emailAccount } = userResult;
- const categoriesResult = await getCategories({ emailAccountId });
+ const categoriesResult = await getCategories();
const { categories } = categoriesResult;
</file context>
| updatedProgress.completedItems + updatedProgress.failedItems >= | ||
| updatedProgress.totalItems | ||
| ) { | ||
| updatedProgress.status = "completed"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Auto-completion overwrites explicit failure status, so failed bulk operations get reported as completed.
Prompt for AI agents
Address the following comment on apps/web/utils/redis/bulk-operation-progress.ts at line 108:
<comment>Auto-completion overwrites explicit failure status, so failed bulk operations get reported as completed.</comment>
<file context>
@@ -0,0 +1,139 @@
+ updatedProgress.completedItems + updatedProgress.failedItems >=
+ updatedProgress.totalItems
+ ) {
+ updatedProgress.status = "completed";
+ }
+
</file context>
|
|
||
| const category = categoryMap[categoryName]; | ||
|
|
||
| if (!category) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Dropping rows where categoryMap lacks an entry causes the entire "Uncategorized" group (which we just created above) to disappear, so senders without a category never render. The table should still render those groups instead of returning null.
Prompt for AI agents
Address the following comment on apps/web/components/DeepCleanGroupedTable.tsx at line 378:
<comment>Dropping rows where categoryMap lacks an entry causes the entire "Uncategorized" group (which we just created above) to disappear, so senders without a category never render. The table should still render those groups instead of returning null.</comment>
<file context>
@@ -0,0 +1,679 @@
+
+ const category = categoryMap[categoryName];
+
+ if (!category) {
+ return null;
+ }
</file context>
| name="autoCategorizeSenders" | ||
| label="Auto categorize" | ||
| enabled={autoCategorizeSenders} | ||
| onChange={async (enabled) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The auto-categorize switch is controlled by enabled={autoCategorizeSenders}, but the onChange handler only calls setAutoCategorizeAction and never refreshes or updates local state, so the UI snaps back and the user cannot actually change the setting.
Prompt for AI agents
Address the following comment on apps/web/app/(app)/[emailAccountId]/deep-clean/Uncategorized.tsx at line 149:
<comment>The auto-categorize switch is controlled by enabled={autoCategorizeSenders}, but the onChange handler only calls setAutoCategorizeAction and never refreshes or updates local state, so the UI snaps back and the user cannot actually change the setting.</comment>
<file context>
@@ -0,0 +1,198 @@
+ name="autoCategorizeSenders"
+ label="Auto categorize"
+ enabled={autoCategorizeSenders}
+ onChange={async (enabled) => {
+ await setAutoCategorizeAction(emailAccountId, {
+ autoCategorizeSenders: enabled,
</file context>
| await bulkCategorizeSendersAction(emailAccountId); | ||
|
|
||
| if (result?.serverError) { | ||
| setIsCategorizing(false); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the categorize action fails we keep isBulkCategorizing stuck at true, so the Deep Clean progress UI never stops. Reset the bulk categorizing flag before throwing to avoid a permanently spinning progress indicator.
Prompt for AI agents
Address the following comment on apps/web/app/(app)/[emailAccountId]/deep-clean/CategorizeWithAiButton.tsx at line 47:
<comment>If the categorize action fails we keep isBulkCategorizing stuck at true, so the Deep Clean progress UI never stops. Reset the bulk categorizing flag before throwing to avoid a permanently spinning progress indicator.</comment>
<file context>
@@ -0,0 +1,105 @@
+ await bulkCategorizeSendersAction(emailAccountId);
+
+ if (result?.serverError) {
+ setIsCategorizing(false);
+ throw new Error(result.serverError);
+ }
</file context>
|
|
||
| // If first-time user with few/no senders, automatically fetch and categorize | ||
| if (data.senders.length < 5) { | ||
| hasAutoTriggered.current = true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hasAutoTriggered is marked true before confirming emailAccount.id, so the auto-categorize effect locks itself out whenever the provider is still loading. Move the flag assignment until after the account ID check so the auto-trigger actually fires once the account is available.
Prompt for AI agents
Address the following comment on apps/web/app/(app)/[emailAccountId]/deep-clean/DeepCleanContent.tsx at line 50:
<comment>`hasAutoTriggered` is marked true before confirming `emailAccount.id`, so the auto-categorize effect locks itself out whenever the provider is still loading. Move the flag assignment until after the account ID check so the auto-trigger actually fires once the account is available.</comment>
<file context>
@@ -0,0 +1,100 @@
+
+ // If first-time user with few/no senders, automatically fetch and categorize
+ if (data.senders.length < 5) {
+ hasAutoTriggered.current = true;
+ handleCategorizeMore();
+ }
</file context>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
♻️ Duplicate comments (5)
apps/web/components/DeepCleanGroupedTable.tsx (2)
437-441: "Uncategorized" group is hidden due to missing categoryMap entry.The
groupedEmailslogic (lines 80-95) creates an "Uncategorized" group, butcategoryMaponly contains entries from thecategoriesprop. When this guard returnsnullfor "Uncategorized", those senders never render.Apply this diff to handle the "Uncategorized" case:
const category = categoryMap[categoryName]; - if (!category) { + if (!category && categoryName !== "Uncategorized") { return null; }Then update the
GroupRowcall to handle the undefined category:<GroupRow - category={category} + category={category || { id: "", name: "Uncategorized", description: null, rules: [] }} count={senders.length}
690-690: Pass the actual message ID to ViewEmailButton.Using
thread.idfor both parameters prevents Gmail users from opening the correct message. UsefirstMessage.id(defined on line 683) for themessageIdprop.Apply this diff:
- <ViewEmailButton threadId={thread.id} messageId={thread.id} /> + <ViewEmailButton threadId={thread.id} messageId={firstMessage.id} />apps/web/app/(app)/[emailAccountId]/deep-clean/DeepCleanContent.tsx (3)
5-5: Fix the import path for CategorizeSendersProgress.The component now lives in the
deep-cleanmodule, notsmart-categories. This import will break at build time.Apply this diff:
-import { CategorizeSendersProgress } from "@/app/(app)/[emailAccountId]/smart-categories/CategorizeProgress"; +import { CategorizeSendersProgress } from "@/app/(app)/[emailAccountId]/deep-clean/CategorizeProgress";
42-42: Remove disallowed console.error.The coding guidelines prohibit
console.*usage. Drop theconsole.errorstatement.As per coding guidelines
Apply this diff:
} catch (error) { toast.error("Failed to categorize senders"); - console.error("Categorize more error:", error); }
47-54: Move hasAutoTriggered assignment after emailAccount check.Setting the flag before
handleCategorizeMoreruns means the auto-trigger never fires ifemailAccountloads after the first effect run. The early-return inhandleCategorizeMore(line 24) prevents execution, but the flag locks out future attempts.Move the flag inside
handleCategorizeMoreafter the account check, or pass a success callback:// Auto-trigger categorization on first load if we have very few senders useEffect(() => { if (hasAutoTriggered.current || !data || isLoading) return; // If first-time user with few/no senders, automatically fetch and categorize if (data.senders.length < 5) { - hasAutoTriggered.current = true; handleCategorizeMore(); + hasAutoTriggered.current = true; } }, [data, isLoading, handleCategorizeMore]);Then update
handleCategorizeMoreto only set the flag on success, or add emailAccount as a dependency to the effect.
🧹 Nitpick comments (1)
apps/web/utils/actions/deep-clean.ts (1)
42-49: Validate category existence before querying senders.The action accepts any string for
categorybut doesn't verify it exists in the database. If a non-existent or typo'd category name is passed, the query returns zero senders and succeeds silently, masking potential bugs.Consider adding category existence validation:
logger.info("Bulk category operation", { category, action, emailAccountId, }); + // Validate category exists + const categoryExists = await prisma.category.findFirst({ + where: { + emailAccountId, + name: category, + }, + }); + + if (!categoryExists) { + throw new SafeError(`Category not found: ${category}`); + } + const senders = await prisma.newsletter.findMany({
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
apps/web/app/(app)/[emailAccountId]/deep-clean/DeepCleanContent.tsx(1 hunks)apps/web/app/(app)/[emailAccountId]/deep-clean/page.tsx(1 hunks)apps/web/components/DeepCleanGroupedTable.tsx(1 hunks)apps/web/components/ProgressPanel.tsx(1 hunks)apps/web/utils/actions/deep-clean.ts(1 hunks)apps/web/utils/actions/deep-clean.validation.ts(1 hunks)
🧰 Additional context used
📓 Path-based instructions (21)
apps/web/**/*.{ts,tsx}
📄 CodeRabbit inference engine (apps/web/CLAUDE.md)
apps/web/**/*.{ts,tsx}: Use TypeScript with strict null checks
Path aliases: Use@/for imports from project root
Use proper error handling with try/catch blocks
Format code with Prettier
Leverage TypeScript inference for better DX
Files:
apps/web/utils/actions/deep-clean.validation.tsapps/web/components/ProgressPanel.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/DeepCleanContent.tsxapps/web/utils/actions/deep-clean.tsapps/web/app/(app)/[emailAccountId]/deep-clean/page.tsxapps/web/components/DeepCleanGroupedTable.tsx
apps/web/utils/actions/**/*.ts
📄 CodeRabbit inference engine (apps/web/CLAUDE.md)
apps/web/utils/actions/**/*.ts: Use server actions for all mutations (create/update/delete operations)
next-safe-actionprovides centralized error handling
Use Zod schemas for validation on both client and server
UserevalidatePathin server actions for cache invalidation
apps/web/utils/actions/**/*.ts: Use server actions (withnext-safe-action) for all mutations (create/update/delete operations); do NOT use POST API routes for mutations.
UserevalidatePathin server actions to invalidate cache after mutations.
Files:
apps/web/utils/actions/deep-clean.validation.tsapps/web/utils/actions/deep-clean.ts
!{.cursor/rules/*.mdc}
📄 CodeRabbit inference engine (.cursor/rules/cursor-rules.mdc)
Never place rule files in the project root, in subdirectories outside .cursor/rules, or in any other location
Files:
apps/web/utils/actions/deep-clean.validation.tsapps/web/components/ProgressPanel.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/DeepCleanContent.tsxapps/web/utils/actions/deep-clean.tsapps/web/app/(app)/[emailAccountId]/deep-clean/page.tsxapps/web/components/DeepCleanGroupedTable.tsx
**/*.ts
📄 CodeRabbit inference engine (.cursor/rules/form-handling.mdc)
**/*.ts: The same validation should be done in the server action too
Define validation schemas using Zod
Files:
apps/web/utils/actions/deep-clean.validation.tsapps/web/utils/actions/deep-clean.ts
apps/web/utils/actions/*.validation.ts
📄 CodeRabbit inference engine (.cursor/rules/fullstack-workflow.mdc)
Define Zod schemas for validation in dedicated files and use them for both client and server validation.
Define input validation schemas using Zod in the corresponding
.validation.tsfile. These schemas are used bynext-safe-action(.schema()) and can also be reused on the client for form validation.
Files:
apps/web/utils/actions/deep-clean.validation.ts
**/*.{ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/logging.mdc)
**/*.{ts,tsx}: UsecreateScopedLoggerfor logging in backend TypeScript files
Typically add the logger initialization at the top of the file when usingcreateScopedLogger
Only use.with()on a logger instance within a specific function, not for a global loggerImport Prisma in the project using
import prisma from "@/utils/prisma";
**/*.{ts,tsx}: Don't use TypeScript enums.
Don't use TypeScript const enum.
Don't use the TypeScript directive @ts-ignore.
Don't use primitive type aliases or misleading types.
Don't use empty type parameters in type aliases and interfaces.
Don't use any or unknown as type constraints.
Don't use implicit any type on variable declarations.
Don't let variables evolve into any type through reassignments.
Don't use non-null assertions with the ! postfix operator.
Don't misuse the non-null assertion operator (!) in TypeScript files.
Don't use user-defined types.
Use as const instead of literal types and type annotations.
Use export type for types.
Use import type for types.
Don't declare empty interfaces.
Don't merge interfaces and classes unsafely.
Don't use overload signatures that aren't next to each other.
Use the namespace keyword instead of the module keyword to declare TypeScript namespaces.
Don't use TypeScript namespaces.
Don't export imported variables.
Don't add type annotations to variables, parameters, and class properties that are initialized with literal expressions.
Don't use parameter properties in class constructors.
Use either T[] or Array consistently.
Initialize each enum member value explicitly.
Make sure all enum members are literal values.
Files:
apps/web/utils/actions/deep-clean.validation.tsapps/web/components/ProgressPanel.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/DeepCleanContent.tsxapps/web/utils/actions/deep-clean.tsapps/web/app/(app)/[emailAccountId]/deep-clean/page.tsxapps/web/components/DeepCleanGroupedTable.tsx
apps/web/utils/actions/*.ts
📄 CodeRabbit inference engine (.cursor/rules/server-actions.mdc)
apps/web/utils/actions/*.ts: Implement all server actions using thenext-safe-actionlibrary for type safety, input validation, context management, and error handling. Refer toapps/web/utils/actions/safe-action.tsfor client definitions (actionClient,actionClientUser,adminActionClient).
UseactionClientUserwhen only authenticated user context (userId) is needed.
UseactionClientwhen both authenticated user context and a specificemailAccountIdare needed. TheemailAccountIdmust be bound when calling the action from the client.
UseadminActionClientfor actions restricted to admin users.
Access necessary context (likeuserId,emailAccountId, etc.) provided by the safe action client via thectxobject in the.action()handler.
Server Actions are strictly for mutations (operations that change data, e.g., creating, updating, deleting). Do NOT use Server Actions for data fetching (GET operations). For data fetching, use dedicated GET API Routes combined with SWR Hooks.
UseSafeErrorfor expected/handled errors within actions if needed.next-safe-actionprovides centralized error handling.
Use the.metadata({ name: "actionName" })method to provide a meaningful name for monitoring. Sentry instrumentation is automatically applied viawithServerActionInstrumentationwithin the safe action clients.
If an action modifies data displayed elsewhere, userevalidatePathorrevalidateTagfromnext/cachewithin the action handler as needed.Server action files must start with
use server
Files:
apps/web/utils/actions/deep-clean.validation.tsapps/web/utils/actions/deep-clean.ts
apps/web/utils/**
📄 CodeRabbit inference engine (.cursor/rules/project-structure.mdc)
Create utility functions in
utils/folder for reusable logic
Files:
apps/web/utils/actions/deep-clean.validation.tsapps/web/utils/actions/deep-clean.ts
apps/web/utils/**/*.ts
📄 CodeRabbit inference engine (.cursor/rules/project-structure.mdc)
apps/web/utils/**/*.ts: Use lodash utilities for common operations (arrays, objects, strings)
Import specific lodash functions to minimize bundle size
Files:
apps/web/utils/actions/deep-clean.validation.tsapps/web/utils/actions/deep-clean.ts
**/*.{js,jsx,ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/ultracite.mdc)
**/*.{js,jsx,ts,tsx}: Don't useelements in Next.js projects.
Don't use elements in Next.js projects.
Don't use namespace imports.
Don't access namespace imports dynamically.
Don't use global eval().
Don't use console.
Don't use debugger.
Don't use var.
Don't use with statements in non-strict contexts.
Don't use the arguments object.
Don't use consecutive spaces in regular expression literals.
Don't use the comma operator.
Don't use unnecessary boolean casts.
Don't use unnecessary callbacks with flatMap.
Use for...of statements instead of Array.forEach.
Don't create classes that only have static members (like a static namespace).
Don't use this and super in static contexts.
Don't use unnecessary catch clauses.
Don't use unnecessary constructors.
Don't use unnecessary continue statements.
Don't export empty modules that don't change anything.
Don't use unnecessary escape sequences in regular expression literals.
Don't use unnecessary labels.
Don't use unnecessary nested block statements.
Don't rename imports, exports, and destructured assignments to the same name.
Don't use unnecessary string or template literal concatenation.
Don't use String.raw in template literals when there are no escape sequences.
Don't use useless case statements in switch statements.
Don't use ternary operators when simpler alternatives exist.
Don't use useless this aliasing.
Don't initialize variables to undefined.
Don't use the void operators (they're not familiar).
Use arrow functions instead of function expressions.
Use Date.now() to get milliseconds since the Unix Epoch.
Use .flatMap() instead of map().flat() when possible.
Use literal property access instead of computed property access.
Don't use parseInt() or Number.parseInt() when binary, octal, or hexadecimal literals work.
Use concise optional chaining instead of chained logical expressions.
Use regular expression literals instead of the RegExp constructor when possible.
Don't use number literal object member names th...
Files:
apps/web/utils/actions/deep-clean.validation.tsapps/web/components/ProgressPanel.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/DeepCleanContent.tsxapps/web/utils/actions/deep-clean.tsapps/web/app/(app)/[emailAccountId]/deep-clean/page.tsxapps/web/components/DeepCleanGroupedTable.tsx
!pages/_document.{js,jsx,ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/ultracite.mdc)
!pages/_document.{js,jsx,ts,tsx}: Don't import next/document outside of pages/_document.jsx in Next.js projects.
Don't import next/document outside of pages/_document.jsx in Next.js projects.
Files:
apps/web/utils/actions/deep-clean.validation.tsapps/web/components/ProgressPanel.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/DeepCleanContent.tsxapps/web/utils/actions/deep-clean.tsapps/web/app/(app)/[emailAccountId]/deep-clean/page.tsxapps/web/components/DeepCleanGroupedTable.tsx
apps/web/**/*.tsx
📄 CodeRabbit inference engine (apps/web/CLAUDE.md)
apps/web/**/*.tsx: Follow tailwindcss patterns with prettier-plugin-tailwindcss
Prefer functional components with hooks
Use shadcn/ui components when available
Ensure responsive design with mobile-first approach
Follow consistent naming conventions (PascalCase for components)
Use LoadingContent component for async data
Useresult?.serverErrorwithtoastErrorandtoastSuccess
UseLoadingContentcomponent to handle loading and error states consistently
Passloading,error, and children props toLoadingContent
Files:
apps/web/components/ProgressPanel.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/DeepCleanContent.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/page.tsxapps/web/components/DeepCleanGroupedTable.tsx
apps/web/components/**/*.tsx
📄 CodeRabbit inference engine (apps/web/CLAUDE.md)
Use React Hook Form with Zod validation for form handling
Use the
LoadingContentcomponent to handle loading and error states consistently in data-fetching components.Use PascalCase for components (e.g.
components/Button.tsx)
Files:
apps/web/components/ProgressPanel.tsxapps/web/components/DeepCleanGroupedTable.tsx
**/*.tsx
📄 CodeRabbit inference engine (.cursor/rules/form-handling.mdc)
**/*.tsx: Use React Hook Form with Zod for validation
Validate form inputs before submission
Show validation errors inline next to form fields
Files:
apps/web/components/ProgressPanel.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/DeepCleanContent.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/page.tsxapps/web/components/DeepCleanGroupedTable.tsx
**/*.{jsx,tsx}
📄 CodeRabbit inference engine (.cursor/rules/ultracite.mdc)
**/*.{jsx,tsx}: Don't destructure props inside JSX components in Solid projects.
Don't use both children and dangerouslySetInnerHTML props on the same element.
Don't use Array index in keys.
Don't assign to React component props.
Don't define React components inside other components.
Don't use event handlers on non-interactive elements.
Don't assign JSX properties multiple times.
Don't add extra closing tags for components without children.
Use <>...</> instead of ....
Don't insert comments as text nodes.
Don't use the return value of React.render.
Make sure all dependencies are correctly specified in React hooks.
Make sure all React hooks are called from the top level of component functions.
Don't use unnecessary fragments.
Don't pass children as props.
Use semantic elements instead of role attributes in JSX.
Files:
apps/web/components/ProgressPanel.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/DeepCleanContent.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/page.tsxapps/web/components/DeepCleanGroupedTable.tsx
**/*.{html,jsx,tsx}
📄 CodeRabbit inference engine (.cursor/rules/ultracite.mdc)
**/*.{html,jsx,tsx}: Don't use or elements.
Don't use accessKey attribute on any HTML element.
Don't set aria-hidden="true" on focusable elements.
Don't add ARIA roles, states, and properties to elements that don't support them.
Only use the scope prop on elements.
Don't assign non-interactive ARIA roles to interactive HTML elements.
Make sure label elements have text content and are associated with an input.
Don't assign interactive ARIA roles to non-interactive HTML elements.
Don't assign tabIndex to non-interactive HTML elements.
Don't use positive integers for tabIndex property.
Don't include "image", "picture", or "photo" in img alt prop.
Don't use explicit role property that's the same as the implicit/default role.
Make static elements with click handlers use a valid role attribute.
Always include a title element for SVG elements.
Give all elements requiring alt text meaningful information for screen readers.
Make sure anchors have content that's accessible to screen readers.
Assign tabIndex to non-interactive HTML elements with aria-activedescendant.
Include all required ARIA attributes for elements with ARIA roles.
Make sure ARIA properties are valid for the element's supported roles.
Always include a type attribute for button elements.
Make elements with interactive roles and handlers focusable.
Give heading elements content that's accessible to screen readers (not hidden with aria-hidden).
Always include a lang attribute on the html element.
Always include a title attribute for iframe elements.
Accompany onClick with at least one of: onKeyUp, onKeyDown, or onKeyPress.
Accompany onMouseOver/onMouseOut with onFocus/onBlur.
Include caption tracks for audio and video elements.
Make sure all anchors are valid and navigable.
Ensure all ARIA properties (aria-*) are valid.
Use valid, non-abstract ARIA roles for elements with ARIA roles.
Use valid ARIA state and property values.
Use valid values for the autocomplete attribute on input eleme...Files:
apps/web/components/ProgressPanel.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/DeepCleanContent.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/page.tsxapps/web/components/DeepCleanGroupedTable.tsxapps/web/app/**
📄 CodeRabbit inference engine (apps/web/CLAUDE.md)
NextJS app router structure with (app) directory
Files:
apps/web/app/(app)/[emailAccountId]/deep-clean/DeepCleanContent.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/page.tsxapps/web/app/(app)/*/**
📄 CodeRabbit inference engine (.cursor/rules/page-structure.mdc)
Components for the page are either put in page.tsx, or in the apps/web/app/(app)/PAGE_NAME folder
Files:
apps/web/app/(app)/[emailAccountId]/deep-clean/DeepCleanContent.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/page.tsxapps/web/app/(app)/*/**/*.tsx
📄 CodeRabbit inference engine (.cursor/rules/page-structure.mdc)
If you need to use onClick in a component, that component is a client component and file must start with 'use client'
Files:
apps/web/app/(app)/[emailAccountId]/deep-clean/DeepCleanContent.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/page.tsxapps/web/app/(app)/*/**/**/*.tsx
📄 CodeRabbit inference engine (.cursor/rules/page-structure.mdc)
If we're in a deeply nested component we will use swr to fetch via API
Files:
apps/web/app/(app)/[emailAccountId]/deep-clean/DeepCleanContent.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/page.tsxapps/web/app/**/*.tsx
📄 CodeRabbit inference engine (.cursor/rules/project-structure.mdc)
Components with
onClickmust be client components withuse clientdirectiveFiles:
apps/web/app/(app)/[emailAccountId]/deep-clean/DeepCleanContent.tsxapps/web/app/(app)/[emailAccountId]/deep-clean/page.tsx🧠 Learnings (8)
📚 Learning: 2025-07-18T15:05:16.146Z
Learnt from: CR PR: elie222/inbox-zero#0 File: .cursor/rules/fullstack-workflow.mdc:0-0 Timestamp: 2025-07-18T15:05:16.146Z Learning: Applies to apps/web/utils/actions/*.validation.ts : Define Zod schemas for validation in dedicated files and use them for both client and server validation.Applied to files:
apps/web/utils/actions/deep-clean.validation.ts📚 Learning: 2025-07-18T15:04:30.467Z
Learnt from: CR PR: elie222/inbox-zero#0 File: apps/web/CLAUDE.md:0-0 Timestamp: 2025-07-18T15:04:30.467Z Learning: Applies to apps/web/utils/actions/**/*.ts : Use Zod schemas for validation on both client and serverApplied to files:
apps/web/utils/actions/deep-clean.validation.ts📚 Learning: 2025-07-18T17:27:58.249Z
Learnt from: CR PR: elie222/inbox-zero#0 File: .cursor/rules/server-actions.mdc:0-0 Timestamp: 2025-07-18T17:27:58.249Z Learning: Applies to apps/web/utils/actions/*.validation.ts : Define input validation schemas using Zod in the corresponding `.validation.ts` file. These schemas are used by `next-safe-action` (`.schema()`) and can also be reused on the client for form validation.Applied to files:
apps/web/utils/actions/deep-clean.validation.ts📚 Learning: 2025-07-18T15:04:57.115Z
Learnt from: CR PR: elie222/inbox-zero#0 File: .cursor/rules/form-handling.mdc:0-0 Timestamp: 2025-07-18T15:04:57.115Z Learning: Applies to **/*.ts : Define validation schemas using ZodApplied to files:
apps/web/utils/actions/deep-clean.validation.ts📚 Learning: 2025-09-17T22:05:28.646Z
Learnt from: CR PR: elie222/inbox-zero#0 File: .cursor/rules/llm.mdc:0-0 Timestamp: 2025-09-17T22:05:28.646Z Learning: Applies to apps/web/utils/ai/**/*.{ts,tsx} : Use proper error types and logging for failuresApplied to files:
apps/web/app/(app)/[emailAccountId]/deep-clean/DeepCleanContent.tsx📚 Learning: 2025-07-20T09:00:16.505Z
Learnt from: CR PR: elie222/inbox-zero#0 File: .cursor/rules/project-structure.mdc:0-0 Timestamp: 2025-07-20T09:00:16.505Z Learning: Applies to apps/web/app/(app)/*/page.tsx : Create new pages at: `apps/web/app/(app)/PAGE_NAME/page.tsx`Applied to files:
apps/web/app/(app)/[emailAccountId]/deep-clean/page.tsx📚 Learning: 2025-07-18T15:07:00.269Z
Learnt from: CR PR: elie222/inbox-zero#0 File: .cursor/rules/page-structure.mdc:0-0 Timestamp: 2025-07-18T15:07:00.269Z Learning: Applies to apps/web/app/(app)/*/page.tsx : Create new pages at: apps/web/app/(app)/PAGE_NAME/page.tsxApplied to files:
apps/web/app/(app)/[emailAccountId]/deep-clean/page.tsx📚 Learning: 2025-07-18T15:07:00.269Z
Learnt from: CR PR: elie222/inbox-zero#0 File: .cursor/rules/page-structure.mdc:0-0 Timestamp: 2025-07-18T15:07:00.269Z Learning: Applies to apps/web/app/(app)/*/** : Components for the page are either put in page.tsx, or in the apps/web/app/(app)/PAGE_NAME folderApplied to files:
apps/web/app/(app)/[emailAccountId]/deep-clean/page.tsx🧬 Code graph analysis (4)
apps/web/app/(app)/[emailAccountId]/deep-clean/DeepCleanContent.tsx (10)
apps/web/providers/EmailAccountProvider.tsx (1)
useAccount(79-89)apps/web/hooks/useDeepClean.ts (1)
useDeepCleanSenders(5-7)apps/web/utils/actions/deep-clean.ts (1)
categorizeMoreSendersAction(176-293)apps/web/components/LoadingContent.tsx (1)
LoadingContent(13-27)apps/web/components/PageWrapper.tsx (1)
PageWrapper(3-20)apps/web/components/PageHeader.tsx (1)
PageHeader(14-34)apps/web/components/ClientOnly.tsx (1)
ClientOnly(5-13)apps/web/app/(app)/[emailAccountId]/deep-clean/BulkOperationProgress.tsx (1)
BulkOperationProgress(7-63)apps/web/app/(app)/[emailAccountId]/deep-clean/CategorizeProgress.tsx (1)
CategorizeSendersProgress(19-80)apps/web/components/DeepCleanGroupedTable.tsx (1)
DeepCleanGroupedTable(58-490)apps/web/utils/actions/deep-clean.ts (6)
apps/web/utils/actions/safe-action.ts (1)
actionClient(62-113)apps/web/utils/actions/deep-clean.validation.ts (3)
bulkCategorySchema(3-6)bulkSendersSchema(9-13)categorizeMoreSendersSchema(16-18)apps/web/utils/user/validate.ts (1)
validateUserAndAiAccess(5-41)apps/web/utils/upstash/bulk-operations.ts (2)
publishArchiveCategoryQueue(20-57)publishMarkAsReadCategoryQueue(62-99)apps/web/utils/error.ts (1)
SafeError(86-96)apps/web/utils/upstash/deep-clean-categorization.ts (1)
getTopSendersForDeepClean(51-108)apps/web/app/(app)/[emailAccountId]/deep-clean/page.tsx (2)
apps/web/app/(app)/[emailAccountId]/PermissionsCheck.tsx (1)
PermissionsCheck(12-33)apps/web/app/(app)/[emailAccountId]/deep-clean/DeepCleanContent.tsx (1)
DeepCleanContent(18-106)apps/web/components/DeepCleanGroupedTable.tsx (8)
apps/web/utils/category.server.ts (1)
CategoryWithRules(7-14)apps/web/providers/EmailAccountProvider.tsx (1)
useAccount(79-89)apps/web/utils/url.ts (2)
getGmailSearchUrl(80-84)getEmailUrl(46-53)apps/web/utils/actions/deep-clean.ts (1)
bulkSendersAction(106-174)apps/web/utils/actions/categorize.ts (1)
changeSenderCategoryAction(131-152)apps/web/hooks/useThreads.ts (1)
useThreads(8-28)apps/web/components/ViewEmailButton.tsx (1)
ViewEmailButton(8-39)apps/web/hooks/useDeepClean.ts (1)
useBulkOperationProgress(9-16)⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: cubic · AI code reviewer
🔇 Additional comments (4)
apps/web/components/ProgressPanel.tsx (1)
28-28: LGTM – Padding adjustment.Removing horizontal padding aligns with the updated layout requirements shown in other Deep Clean components.
apps/web/app/(app)/[emailAccountId]/deep-clean/page.tsx (1)
1-11: LGTM – Clean page structure.The page correctly wraps DeepCleanContent with PermissionsCheck and follows the project's app router conventions.
Based on learnings
apps/web/utils/actions/deep-clean.validation.ts (1)
1-21: LGTM – Well-structured validation schemas.The Zod schemas provide clear type-safe validation for bulk operations with proper enum constraints and defaults.
Based on learnings
apps/web/utils/actions/deep-clean.ts (1)
176-293: LGTM – Robust categorization logic with fallback.The action correctly handles the cold-start case by augmenting from EmailMessage when Newsletter has few entries, properly deduplicates, and initializes Redis progress tracking.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
8 issues found across 30 files
Prompt for AI agents (all 8 issues)
Understand the root cause of the following 8 issues and fix them.
<file name="apps/web/utils/actions/deep-clean.ts">
<violation number="1" location="apps/web/utils/actions/deep-clean.ts:253">
Overwriting the existing sender list when fetching fallback senders drops the original getTopSenders results, so those senders never get queued for categorization. Append the new senders instead of replacing the array.</violation>
</file>
<file name="apps/web/app/(app)/[emailAccountId]/deep-clean/CategorizeWithAiButton.tsx">
<violation number="1" location="apps/web/app/(app)/[emailAccountId]/deep-clean/CategorizeWithAiButton.tsx:42">
If `bulkCategorizeSendersAction` fails, `setIsBulkCategorizing(true)` is never reverted, leaving the progress indicator stuck on and blocking retries.</violation>
</file>
<file name="apps/web/app/(app)/[emailAccountId]/deep-clean/CreateCategoryButton.tsx">
<violation number="1" location="apps/web/app/(app)/[emailAccountId]/deep-clean/CreateCategoryButton.tsx:156">
Editing an existing category will always create a duplicate entry. createCategoryAction relies on data.id to hit the update path, but this form never registers the id field—only defaultValues is set—so react-hook-form omits it from the submit payload and newCategory.id is undefined even when editing.</violation>
</file>
<file name="apps/web/utils/redis/bulk-operation-progress.ts">
<violation number="1" location="apps/web/utils/redis/bulk-operation-progress.ts:108">
Explicitly setting status to "failed" is ignored once processed items reach total, so failed jobs are misreported as completed.</violation>
<violation number="2" location="apps/web/utils/redis/bulk-operation-progress.ts:122">
Fetching operation keys with redis.keys will block the Redis instance on large keysets; switch to a SCAN-based iteration to avoid O(N) blocking.</violation>
</file>
<file name="apps/web/app/(app)/[emailAccountId]/deep-clean/BulkOperationProgress.tsx">
<violation number="1" location="apps/web/app/(app)/[emailAccountId]/deep-clean/BulkOperationProgress.tsx:35">
Move this setTimeout out of render; invoking it here queues a new timeout on every re-render for completed operations, leaking timers and doing unnecessary work. Use a useEffect tied to completion state instead.</violation>
</file>
<file name="apps/web/components/SideNav.tsx">
<violation number="1" location="apps/web/components/SideNav.tsx:93">
The Deep Clean nav item now points to `/deep-clean`, but the feature-flag filter still only hides `/clean` links. This makes the Deep Clean entry visible even when the cleaner flag is off, surfacing an unavailable feature.</violation>
</file>
<file name="apps/web/utils/actions/deep-clean.validation.ts">
<violation number="1" location="apps/web/utils/actions/deep-clean.validation.ts:17">
`limit` should be validated as a positive integer; with the current schema a negative/zero client value propagates to Prisma `take`, which rejects non-positive numbers and causes the action to fail.</violation>
</file>
React with 👍 or 👎 to teach cubic. Mention @cubic-dev-ai to give feedback, ask questions, or re-run the review.
| ), | ||
| ); | ||
|
|
||
| sendersToCategorize = newSenders.map((s) => s.from); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overwriting the existing sender list when fetching fallback senders drops the original getTopSenders results, so those senders never get queued for categorization. Append the new senders instead of replacing the array.
Prompt for AI agents
Address the following comment on apps/web/utils/actions/deep-clean.ts at line 253:
<comment>Overwriting the existing sender list when fetching fallback senders drops the original getTopSenders results, so those senders never get queued for categorization. Append the new senders instead of replacing the array.</comment>
<file context>
@@ -0,0 +1,293 @@
+ ),
+ );
+
+ sendersToCategorize = newSenders.map((s) => s.from);
+
+ logger.info("Added senders from EmailMessage", {
</file context>
| sendersToCategorize = newSenders.map((s) => s.from); | |
| sendersToCategorize = Array.from(new Set([...sendersToCategorize, ...newSenders.map((s) => s.from)])); |
| toast.promise( | ||
| async () => { | ||
| setIsCategorizing(true); | ||
| setIsBulkCategorizing(true); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If bulkCategorizeSendersAction fails, setIsBulkCategorizing(true) is never reverted, leaving the progress indicator stuck on and blocking retries.
Prompt for AI agents
Address the following comment on apps/web/app/(app)/[emailAccountId]/deep-clean/CategorizeWithAiButton.tsx at line 42:
<comment>If `bulkCategorizeSendersAction` fails, `setIsBulkCategorizing(true)` is never reverted, leaving the progress indicator stuck on and blocking retries.</comment>
<file context>
@@ -0,0 +1,105 @@
+ toast.promise(
+ async () => {
+ setIsCategorizing(true);
+ setIsBulkCategorizing(true);
+ const result =
+ await bulkCategorizeSendersAction(emailAccountId);
</file context>
| } = useForm<CreateCategoryBody>({ | ||
| resolver: zodResolver(createCategoryBody), | ||
| defaultValues: { | ||
| id: category?.id, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Editing an existing category will always create a duplicate entry. createCategoryAction relies on data.id to hit the update path, but this form never registers the id field—only defaultValues is set—so react-hook-form omits it from the submit payload and newCategory.id is undefined even when editing.
Prompt for AI agents
Address the following comment on apps/web/app/(app)/[emailAccountId]/deep-clean/CreateCategoryButton.tsx at line 156:
<comment>Editing an existing category will always create a duplicate entry. createCategoryAction relies on data.id to hit the update path, but this form never registers the id field—only defaultValues is set—so react-hook-form omits it from the submit payload and newCategory.id is undefined even when editing.</comment>
<file context>
@@ -0,0 +1,237 @@
+ } = useForm<CreateCategoryBody>({
+ resolver: zodResolver(createCategoryBody),
+ defaultValues: {
+ id: category?.id,
+ name: category?.name,
+ description: category?.description,
</file context>
| emailAccountId: string; | ||
| }): Promise<Array<RedisBulkOperationProgress & { operationId: string }>> { | ||
| const pattern = `bulk-operation-progress:${emailAccountId}:*`; | ||
| const keys = await redis.keys(pattern); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fetching operation keys with redis.keys will block the Redis instance on large keysets; switch to a SCAN-based iteration to avoid O(N) blocking.
Prompt for AI agents
Address the following comment on apps/web/utils/redis/bulk-operation-progress.ts at line 122:
<comment>Fetching operation keys with redis.keys will block the Redis instance on large keysets; switch to a SCAN-based iteration to avoid O(N) blocking.</comment>
<file context>
@@ -0,0 +1,139 @@
+ emailAccountId: string;
+}): Promise<Array<RedisBulkOperationProgress & { operationId: string }>> {
+ const pattern = `bulk-operation-progress:${emailAccountId}:*`;
+ const keys = await redis.keys(pattern);
+
+ if (keys.length === 0) return [];
</file context>
| updatedProgress.completedItems + updatedProgress.failedItems >= | ||
| updatedProgress.totalItems | ||
| ) { | ||
| updatedProgress.status = "completed"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Explicitly setting status to "failed" is ignored once processed items reach total, so failed jobs are misreported as completed.
Prompt for AI agents
Address the following comment on apps/web/utils/redis/bulk-operation-progress.ts at line 108:
<comment>Explicitly setting status to "failed" is ignored once processed items reach total, so failed jobs are misreported as completed.</comment>
<file context>
@@ -0,0 +1,139 @@
+ updatedProgress.completedItems + updatedProgress.failedItems >=
+ updatedProgress.totalItems
+ ) {
+ updatedProgress.status = "completed";
+ }
+
</file context>
|
|
||
| // Hide completed operations after 5 seconds | ||
| if (isCompleted) { | ||
| setTimeout(() => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Move this setTimeout out of render; invoking it here queues a new timeout on every re-render for completed operations, leaking timers and doing unnecessary work. Use a useEffect tied to completion state instead.
Prompt for AI agents
Address the following comment on apps/web/app/(app)/[emailAccountId]/deep-clean/BulkOperationProgress.tsx at line 35:
<comment>Move this setTimeout out of render; invoking it here queues a new timeout on every re-render for completed operations, leaking timers and doing unnecessary work. Use a useEffect tied to completion state instead.</comment>
<file context>
@@ -0,0 +1,63 @@
+
+ // Hide completed operations after 5 seconds
+ if (isCompleted) {
+ setTimeout(() => {
+ // This will be handled by React's re-render when the operation is removed from Redis
+ }, 5000);
</file context>
| : []), | ||
| { | ||
| name: "Deep Clean", | ||
| href: prefixPath(currentEmailAccountId, "/deep-clean"), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Deep Clean nav item now points to /deep-clean, but the feature-flag filter still only hides /clean links. This makes the Deep Clean entry visible even when the cleaner flag is off, surfacing an unavailable feature.
Prompt for AI agents
Address the following comment on apps/web/components/SideNav.tsx at line 93:
<comment>The Deep Clean nav item now points to `/deep-clean`, but the feature-flag filter still only hides `/clean` links. This makes the Deep Clean entry visible even when the cleaner flag is off, surfacing an unavailable feature.</comment>
<file context>
@@ -88,15 +88,20 @@ export const useNavigation = () => {
- : []),
+ {
+ name: "Deep Clean",
+ href: prefixPath(currentEmailAccountId, "/deep-clean"),
+ icon: BrushIcon,
+ },
</file context>
| export type BulkSendersSchema = z.infer<typeof bulkSendersSchema>; | ||
|
|
||
| export const categorizeMoreSendersSchema = z.object({ | ||
| limit: z.number().optional().default(100), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
limit should be validated as a positive integer; with the current schema a negative/zero client value propagates to Prisma take, which rejects non-positive numbers and causes the action to fail.
Prompt for AI agents
Address the following comment on apps/web/utils/actions/deep-clean.validation.ts at line 17:
<comment>`limit` should be validated as a positive integer; with the current schema a negative/zero client value propagates to Prisma `take`, which rejects non-positive numbers and causes the action to fail.</comment>
<file context>
@@ -0,0 +1,21 @@
+export type BulkSendersSchema = z.infer<typeof bulkSendersSchema>;
+
+export const categorizeMoreSendersSchema = z.object({
+ limit: z.number().optional().default(100),
+});
+export type CategorizeMoreSendersSchema = z.infer<
</file context>
| limit: z.number().optional().default(100), | |
| limit: z.number().int().positive().optional().default(100), |
Summary by CodeRabbit
New Features
Improvements
Navigation