Skip to content

Conversation

@elie222
Copy link
Owner

@elie222 elie222 commented Oct 17, 2025

Summary by CodeRabbit

  • New Features

    • Deep Clean UI: grouped sender table, uncategorized list, multi-select bulk actions, and visible progress panels
    • AI-powered bulk categorization button (premium-gated) with queued background processing and progress feedback
    • Background bulk archive and mark-as-read workflows with tracked operation progress
    • Category creation/edit dialog and per-sender priority support
  • Improvements

    • More robust sender name extraction from email addresses
    • Real-time progress polling, smoother pagination and “load more” UX
  • Navigation

    • Deep Clean added to main navigation

@vercel
Copy link

vercel bot commented Oct 17, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Updated (UTC)
inbox-zero Ready Ready Preview Oct 17, 2025 2:44pm

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 17, 2025

Note

Other AI code review bot(s) detected

CodeRabbit 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.

Walkthrough

Adds 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

Cohort / File(s) Summary
Frontend — Deep Clean UI
apps/web/app/(app)/[emailAccountId]/deep-clean/BulkOperationProgress.tsx, apps/web/app/(app)/[emailAccountId]/deep-clean/CategorizeProgress.tsx, apps/web/app/(app)/[emailAccountId]/deep-clean/CategorizeWithAiButton.tsx, apps/web/app/(app)/[emailAccountId]/deep-clean/CreateCategoryButton.tsx, apps/web/app/(app)/[emailAccountId]/deep-clean/DeepCleanContent.tsx, apps/web/app/(app)/[emailAccountId]/deep-clean/Uncategorized.tsx, apps/web/app/(app)/[emailAccountId]/deep-clean/page.tsx
New client components/hooks for polling and displaying bulk progress, AI categorization button (premium-gated), category creation UI, deep-clean content page, uncategorized list, and page wiring.
Frontend — Grouped Table & Progress UI
apps/web/components/DeepCleanGroupedTable.tsx, apps/web/components/ProgressPanel.tsx
New grouped/expandable table with multi-select, per-group and per-sender actions, message previews, and minor ProgressPanel spacing tweak.
Client Hooks
apps/web/hooks/useDeepClean.ts
Added SWR hooks useDeepCleanSenders and useBulkOperationProgress (supports refresh interval).
API — Deep Clean & Categorization Endpoints
apps/web/app/api/user/deep-clean/progress/route.ts, apps/web/app/api/user/deep-clean/senders/route.ts, apps/web/app/api/user/categorize/senders/batch/handle-batch.ts
New GET endpoints for progress and deep-clean senders; batch categorize handler adjusted to call getCategories() (no args), pass priority to updateSenderCategory, and default unknown category string.
Webhooks — Background Executors
apps/web/app/api/webhooks/deep-clean/archive/route.ts, apps/web/app/api/webhooks/deep-clean/mark-read/route.ts
New POST webhook handlers that fetch thread IDs, process threads in batches (archive/mark-read), and update Redis progress with success/error counts.
Server Actions / Jobs Enqueueing
apps/web/utils/actions/deep-clean.ts, apps/web/utils/actions/deep-clean.validation.ts
New server actions bulkCategoryAction, bulkSendersAction, categorizeMoreSendersAction (enqueue jobs, init Redis progress) and Zod validation schemas for bulk payloads.
Queue Publishers (Upstash / QStash)
apps/web/utils/upstash/bulk-operations.ts, apps/web/utils/upstash/deep-clean-categorization.ts
Publish archive/mark-read and deep-clean categorization jobs to QStash/Upstash with constructed webhook URLs and logging.
Redis Progress Store
apps/web/utils/redis/bulk-operation-progress.ts
Redis-backed progress utilities: schema, create/get/update/getAll, TTL management, and completion logic.
AI Categorization — Prompts & Models
apps/web/utils/ai/categorize-sender/prompts.ts, apps/web/utils/ai/categorize-sender/ai-categorize-senders.ts, apps/web/utils/ai/categorize-sender/ai-categorize-single-sender.ts
Centralized prompts and zod schemas, AI responses now include optional priority, model selection adjusted, and priority propagated through results.
Categorization Logic & Persistence
apps/web/utils/categorize/senders/categorize.ts, apps/web/utils/actions/deep-clean.ts
Always use server-side getCategories(), propagate priority into updateSenderCategory, upsert missing categories and persist numeric priority on Newsletter records.
Priority Utilities & DB
apps/web/utils/priority.ts, apps/web/prisma/schema.prisma
Added PriorityLevel type, mapping helpers (priorityToNumber, numberToPriority), and priority Int? on Newsletter model.
Email Parsing & Tests
apps/web/utils/email.ts, apps/web/utils/email.test.ts
Improved extractNameFromEmail to remove surrounding quotes and handle bracketed/quoted patterns; added unit tests for many edge cases.
Navigation
apps/web/components/SideNav.tsx
Made "Deep Clean" nav item always visible (removed provider-based conditional rendering).
Tests / Adjustments
apps/web/__tests__/ai-categorize-senders.test.ts, apps/web/utils/email.test.ts
Adjusted categorization test expectations for "Unknown" cases and added email parsing tests.

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)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Poem

🐰
I hopped through code at break of day,
Grouped senders neat, then bounced away.
Queues hum softly and Redis keeps score,
Archive and mark — tidy inbox once more.
Happy hops — Deep Clean, evermore!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title Check ❓ Inconclusive The title "wip deep clean v2" contains both descriptive and non-descriptive elements. The "deep clean v2" portion clearly identifies the main feature being implemented across the changeset (new components, API routes, utilities, and database schema changes for email bulk operations and sender categorization). However, the "wip" prefix is a vague status indicator ("work in progress") rather than a description of the actual functionality, which creates ambiguity about whether this describes the feature itself or merely indicates the PR's development status. A reviewer scanning the history might not understand if this is a final feature name or a placeholder, making the title less clear than ideal for capturing the primary change. Consider revising the title to remove the "wip" prefix and either use the feature name directly ("Add Deep Clean v2 feature") or be more specific about the primary functionality (e.g., "Add bulk email operations and AI sender categorization for deep clean"). This will make the title more suitable for commit history while clearly conveying the main change without ambiguity about the PR's readiness.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/deep-clean

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a 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">
&quot;Other&quot; 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 &#39;quoted name&#39; 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 &#39;quoted name&#39; 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.

.metadata({ name: "archiveCategory" })
.schema(
z.object({
category: z.string(),
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Oct 17, 2025

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: &quot;archiveCategory&quot; })
+  .schema(
+    z.object({
+      category: z.string(),
+    }),
+  )
</file context>
Fix with Cubic


// Hide completed operations after 5 seconds
if (isCompleted) {
setTimeout(() => {
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Oct 17, 2025

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(() =&gt; {
+            // This will be handled by React&#39;s re-render when the operation is removed from Redis
+          }, 5000);
</file context>
Fix with Cubic

// Mark selected as read
const markSelectedAsRead = async () => {
// TODO: Implement mark as read functionality
toastSuccess({
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Oct 17, 2025

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 () =&gt; {
+    // 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} />
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Oct 17, 2025

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 (
+          &lt;TableRow key={thread.id} className=&quot;bg-muted/50&quot;&gt;
+            &lt;TableCell className=&quot;py-3&quot;&gt;
+              &lt;ViewEmailButton threadId={thread.id} messageId={thread.id} /&gt;
+            &lt;/TableCell&gt;
+            &lt;TableCell className=&quot;py-3&quot;&gt;
</file context>
Suggested change
<ViewEmailButton threadId={thread.id} messageId={thread.id} />
<ViewEmailButton threadId={thread.id} messageId={firstMessage.id} />
Fix with Cubic

emailAccountId: string;
}): Promise<Array<RedisBulkOperationProgress & { operationId: string }>> {
const pattern = `bulk-operation-progress:${emailAccountId}:*`;
const keys = await redis.keys(pattern);
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Oct 17, 2025

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&lt;Array&lt;RedisBulkOperationProgress &amp; { operationId: string }&gt;&gt; {
+  const pattern = `bulk-operation-progress:${emailAccountId}:*`;
+  const keys = await redis.keys(pattern);
+
+  if (keys.length === 0) return [];
</file context>
Fix with Cubic

updatedProgress.completedItems + updatedProgress.failedItems >=
updatedProgress.totalItems
) {
updatedProgress.status = "completed";
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Oct 17, 2025

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 &gt;=
+    updatedProgress.totalItems
+  ) {
+    updatedProgress.status = &quot;completed&quot;;
+  }
+
</file context>
Suggested change
updatedProgress.status = "completed";
updatedProgress.status = updatedProgress.status === "failed" ? "failed" : "completed";
Fix with Cubic

'"quoted name"',
);
expect(extractNameFromEmail("''quoted name'' <[email protected]>")).toBe(
"'quoted name'",
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Oct 17, 2025

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 &#39;quoted name&#39; to match real behavior.</comment>

<file context>
@@ -30,6 +30,70 @@ describe(&quot;email utils&quot;, () =&gt; {
+        &#39;&quot;quoted name&quot;&#39;,
+      );
+      expect(extractNameFromEmail(&quot;&#39;&#39;quoted name&#39;&#39; &lt;[email protected]&gt;&quot;)).toBe(
+        &quot;&#39;quoted name&#39;&quot;,
+      );
+    });
</file context>
Suggested change
"'quoted name'",
"quoted name",
Fix with Cubic


it("handles multiple quotes correctly", () => {
expect(extractNameFromEmail('""quoted name"" <[email protected]>')).toBe(
'"quoted name"',
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Oct 17, 2025

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 &#39;quoted name&#39; instead.</comment>

<file context>
@@ -30,6 +30,70 @@ describe(&quot;email utils&quot;, () =&gt; {
+
+    it(&quot;handles multiple quotes correctly&quot;, () =&gt; {
+      expect(extractNameFromEmail(&#39;&quot;&quot;quoted name&quot;&quot; &lt;[email protected]&gt;&#39;)).toBe(
+        &#39;&quot;quoted name&quot;&#39;,
+      );
+      expect(extractNameFromEmail(&quot;&#39;&#39;quoted name&#39;&#39; &lt;[email protected]&gt;&quot;)).toBe(
</file context>
Suggested change
'"quoted name"',
"quoted name",
Fix with Cubic

logger.info("Archiving category", { category, emailAccountId });

// Get all senders in this category
const senders = await prisma.newsletter.findMany({
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Oct 17, 2025

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>&quot;Other&quot; category archives every sender instead of just uncategorized ones</comment>

<file context>
@@ -0,0 +1,310 @@
+      logger.info(&quot;Archiving category&quot;, { category, emailAccountId });
+
+      // Get all senders in this category
+      const senders = await prisma.newsletter.findMany({
+        where: {
+          emailAccountId,
</file context>
Fix with Cubic

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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 missing

With the new default-category flow, categorizeSender now falls back to an empty list when userCategories is absent. That means we hand zero categories to aiCategorizeSender, so it cannot return a category or priority—categorization silently fails and we always return undefined. 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

📥 Commits

Reviewing files that changed from the base of the PR and between 45547ea and 30da521.

📒 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.tsx
  • apps/web/utils/ai/categorize-sender/ai-categorize-senders.ts
  • apps/web/app/(app)/[emailAccountId]/deep-clean/DeepCleanContent.tsx
  • apps/web/app/(app)/[emailAccountId]/deep-clean/CategorizeWithAiButton.tsx
  • apps/web/app/(app)/[emailAccountId]/deep-clean/page.tsx
  • apps/web/utils/email.test.ts
  • apps/web/app/api/user/categorize/senders/batch/handle-batch.ts
  • apps/web/utils/actions/deep-clean.ts
  • apps/web/app/(app)/[emailAccountId]/deep-clean/BulkOperationProgress.tsx
  • apps/web/utils/categorize/senders/categorize.ts
  • apps/web/utils/priority.ts
  • apps/web/app/api/user/deep-clean/progress/route.ts
  • apps/web/app/api/webhooks/deep-clean/mark-read/route.ts
  • apps/web/utils/redis/bulk-operation-progress.ts
  • apps/web/utils/upstash/deep-clean-categorization.ts
  • apps/web/utils/email.ts
  • apps/web/app/(app)/[emailAccountId]/deep-clean/CreateCategoryButton.tsx
  • apps/web/utils/upstash/bulk-operations.ts
  • apps/web/hooks/useDeepClean.ts
  • apps/web/components/SideNav.tsx
  • apps/web/app/(app)/[emailAccountId]/deep-clean/Uncategorized.tsx
  • apps/web/app/api/webhooks/deep-clean/archive/route.ts
  • apps/web/app/api/user/deep-clean/senders/route.ts
  • apps/web/components/DeepCleanGroupedTable.tsx
  • apps/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.tsx
  • apps/web/app/(app)/[emailAccountId]/deep-clean/DeepCleanContent.tsx
  • apps/web/app/(app)/[emailAccountId]/deep-clean/CategorizeWithAiButton.tsx
  • apps/web/app/(app)/[emailAccountId]/deep-clean/page.tsx
  • apps/web/app/api/user/categorize/senders/batch/handle-batch.ts
  • apps/web/app/(app)/[emailAccountId]/deep-clean/BulkOperationProgress.tsx
  • apps/web/app/api/user/deep-clean/progress/route.ts
  • apps/web/app/api/webhooks/deep-clean/mark-read/route.ts
  • apps/web/app/(app)/[emailAccountId]/deep-clean/CreateCategoryButton.tsx
  • apps/web/app/(app)/[emailAccountId]/deep-clean/Uncategorized.tsx
  • apps/web/app/api/webhooks/deep-clean/archive/route.ts
  • apps/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
Use result?.serverError with toastError and toastSuccess
Use LoadingContent component to handle loading and error states consistently
Pass loading, error, and children props to LoadingContent

Files:

  • apps/web/app/(app)/[emailAccountId]/deep-clean/CategorizeProgress.tsx
  • apps/web/app/(app)/[emailAccountId]/deep-clean/DeepCleanContent.tsx
  • apps/web/app/(app)/[emailAccountId]/deep-clean/CategorizeWithAiButton.tsx
  • apps/web/app/(app)/[emailAccountId]/deep-clean/page.tsx
  • apps/web/app/(app)/[emailAccountId]/deep-clean/BulkOperationProgress.tsx
  • apps/web/app/(app)/[emailAccountId]/deep-clean/CreateCategoryButton.tsx
  • apps/web/components/SideNav.tsx
  • apps/web/app/(app)/[emailAccountId]/deep-clean/Uncategorized.tsx
  • apps/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.tsx
  • apps/web/utils/ai/categorize-sender/ai-categorize-senders.ts
  • apps/web/app/(app)/[emailAccountId]/deep-clean/DeepCleanContent.tsx
  • apps/web/prisma/schema.prisma
  • apps/web/app/(app)/[emailAccountId]/deep-clean/CategorizeWithAiButton.tsx
  • apps/web/app/(app)/[emailAccountId]/deep-clean/page.tsx
  • apps/web/utils/email.test.ts
  • apps/web/app/api/user/categorize/senders/batch/handle-batch.ts
  • apps/web/utils/actions/deep-clean.ts
  • apps/web/app/(app)/[emailAccountId]/deep-clean/BulkOperationProgress.tsx
  • apps/web/utils/categorize/senders/categorize.ts
  • apps/web/utils/priority.ts
  • apps/web/app/api/user/deep-clean/progress/route.ts
  • apps/web/app/api/webhooks/deep-clean/mark-read/route.ts
  • apps/web/utils/redis/bulk-operation-progress.ts
  • apps/web/utils/upstash/deep-clean-categorization.ts
  • apps/web/utils/email.ts
  • apps/web/app/(app)/[emailAccountId]/deep-clean/CreateCategoryButton.tsx
  • apps/web/utils/upstash/bulk-operations.ts
  • apps/web/hooks/useDeepClean.ts
  • apps/web/components/SideNav.tsx
  • apps/web/app/(app)/[emailAccountId]/deep-clean/Uncategorized.tsx
  • apps/web/app/api/webhooks/deep-clean/archive/route.ts
  • apps/web/app/api/user/deep-clean/senders/route.ts
  • apps/web/components/DeepCleanGroupedTable.tsx
  • apps/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.tsx
  • apps/web/app/(app)/[emailAccountId]/deep-clean/DeepCleanContent.tsx
  • apps/web/app/(app)/[emailAccountId]/deep-clean/CategorizeWithAiButton.tsx
  • apps/web/app/(app)/[emailAccountId]/deep-clean/page.tsx
  • apps/web/app/(app)/[emailAccountId]/deep-clean/BulkOperationProgress.tsx
  • apps/web/app/(app)/[emailAccountId]/deep-clean/CreateCategoryButton.tsx
  • apps/web/components/SideNav.tsx
  • apps/web/app/(app)/[emailAccountId]/deep-clean/Uncategorized.tsx
  • apps/web/components/DeepCleanGroupedTable.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/logging.mdc)

**/*.{ts,tsx}: Use createScopedLogger for logging in backend TypeScript files
Typically add the logger initialization at the top of the file when using createScopedLogger
Only use .with() on a logger instance within a specific function, not for a global logger

Import 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.tsx
  • apps/web/utils/ai/categorize-sender/ai-categorize-senders.ts
  • apps/web/app/(app)/[emailAccountId]/deep-clean/DeepCleanContent.tsx
  • apps/web/app/(app)/[emailAccountId]/deep-clean/CategorizeWithAiButton.tsx
  • apps/web/app/(app)/[emailAccountId]/deep-clean/page.tsx
  • apps/web/utils/email.test.ts
  • apps/web/app/api/user/categorize/senders/batch/handle-batch.ts
  • apps/web/utils/actions/deep-clean.ts
  • apps/web/app/(app)/[emailAccountId]/deep-clean/BulkOperationProgress.tsx
  • apps/web/utils/categorize/senders/categorize.ts
  • apps/web/utils/priority.ts
  • apps/web/app/api/user/deep-clean/progress/route.ts
  • apps/web/app/api/webhooks/deep-clean/mark-read/route.ts
  • apps/web/utils/redis/bulk-operation-progress.ts
  • apps/web/utils/upstash/deep-clean-categorization.ts
  • apps/web/utils/email.ts
  • apps/web/app/(app)/[emailAccountId]/deep-clean/CreateCategoryButton.tsx
  • apps/web/utils/upstash/bulk-operations.ts
  • apps/web/hooks/useDeepClean.ts
  • apps/web/components/SideNav.tsx
  • apps/web/app/(app)/[emailAccountId]/deep-clean/Uncategorized.tsx
  • apps/web/app/api/webhooks/deep-clean/archive/route.ts
  • apps/web/app/api/user/deep-clean/senders/route.ts
  • apps/web/components/DeepCleanGroupedTable.tsx
  • apps/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.tsx
  • apps/web/app/(app)/[emailAccountId]/deep-clean/DeepCleanContent.tsx
  • apps/web/app/(app)/[emailAccountId]/deep-clean/CategorizeWithAiButton.tsx
  • apps/web/app/(app)/[emailAccountId]/deep-clean/page.tsx
  • apps/web/app/(app)/[emailAccountId]/deep-clean/BulkOperationProgress.tsx
  • apps/web/app/(app)/[emailAccountId]/deep-clean/CreateCategoryButton.tsx
  • apps/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.tsx
  • apps/web/app/(app)/[emailAccountId]/deep-clean/DeepCleanContent.tsx
  • apps/web/app/(app)/[emailAccountId]/deep-clean/CategorizeWithAiButton.tsx
  • apps/web/app/(app)/[emailAccountId]/deep-clean/page.tsx
  • apps/web/app/(app)/[emailAccountId]/deep-clean/BulkOperationProgress.tsx
  • apps/web/app/(app)/[emailAccountId]/deep-clean/CreateCategoryButton.tsx
  • apps/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.tsx
  • apps/web/app/(app)/[emailAccountId]/deep-clean/DeepCleanContent.tsx
  • apps/web/app/(app)/[emailAccountId]/deep-clean/CategorizeWithAiButton.tsx
  • apps/web/app/(app)/[emailAccountId]/deep-clean/page.tsx
  • apps/web/app/(app)/[emailAccountId]/deep-clean/BulkOperationProgress.tsx
  • apps/web/app/(app)/[emailAccountId]/deep-clean/CreateCategoryButton.tsx
  • apps/web/app/(app)/[emailAccountId]/deep-clean/Uncategorized.tsx
apps/web/app/**/*.tsx

📄 CodeRabbit inference engine (.cursor/rules/project-structure.mdc)

Components with onClick must be client components with use client directive

Files:

  • apps/web/app/(app)/[emailAccountId]/deep-clean/CategorizeProgress.tsx
  • apps/web/app/(app)/[emailAccountId]/deep-clean/DeepCleanContent.tsx
  • apps/web/app/(app)/[emailAccountId]/deep-clean/CategorizeWithAiButton.tsx
  • apps/web/app/(app)/[emailAccountId]/deep-clean/page.tsx
  • apps/web/app/(app)/[emailAccountId]/deep-clean/BulkOperationProgress.tsx
  • apps/web/app/(app)/[emailAccountId]/deep-clean/CreateCategoryButton.tsx
  • apps/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 use elements 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.tsx
  • apps/web/utils/ai/categorize-sender/ai-categorize-senders.ts
  • apps/web/app/(app)/[emailAccountId]/deep-clean/DeepCleanContent.tsx
  • apps/web/app/(app)/[emailAccountId]/deep-clean/CategorizeWithAiButton.tsx
  • apps/web/app/(app)/[emailAccountId]/deep-clean/page.tsx
  • apps/web/utils/email.test.ts
  • apps/web/app/api/user/categorize/senders/batch/handle-batch.ts
  • apps/web/utils/actions/deep-clean.ts
  • apps/web/app/(app)/[emailAccountId]/deep-clean/BulkOperationProgress.tsx
  • apps/web/utils/categorize/senders/categorize.ts
  • apps/web/utils/priority.ts
  • apps/web/app/api/user/deep-clean/progress/route.ts
  • apps/web/app/api/webhooks/deep-clean/mark-read/route.ts
  • apps/web/utils/redis/bulk-operation-progress.ts
  • apps/web/utils/upstash/deep-clean-categorization.ts
  • apps/web/utils/email.ts
  • apps/web/app/(app)/[emailAccountId]/deep-clean/CreateCategoryButton.tsx
  • apps/web/utils/upstash/bulk-operations.ts
  • apps/web/hooks/useDeepClean.ts
  • apps/web/components/SideNav.tsx
  • apps/web/app/(app)/[emailAccountId]/deep-clean/Uncategorized.tsx
  • apps/web/app/api/webhooks/deep-clean/archive/route.ts
  • apps/web/app/api/user/deep-clean/senders/route.ts
  • apps/web/components/DeepCleanGroupedTable.tsx
  • apps/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.tsx
  • apps/web/utils/ai/categorize-sender/ai-categorize-senders.ts
  • apps/web/app/(app)/[emailAccountId]/deep-clean/DeepCleanContent.tsx
  • apps/web/prisma/schema.prisma
  • apps/web/app/(app)/[emailAccountId]/deep-clean/CategorizeWithAiButton.tsx
  • apps/web/app/(app)/[emailAccountId]/deep-clean/page.tsx
  • apps/web/utils/email.test.ts
  • apps/web/app/api/user/categorize/senders/batch/handle-batch.ts
  • apps/web/utils/actions/deep-clean.ts
  • apps/web/app/(app)/[emailAccountId]/deep-clean/BulkOperationProgress.tsx
  • apps/web/utils/categorize/senders/categorize.ts
  • apps/web/utils/priority.ts
  • apps/web/app/api/user/deep-clean/progress/route.ts
  • apps/web/app/api/webhooks/deep-clean/mark-read/route.ts
  • apps/web/utils/redis/bulk-operation-progress.ts
  • apps/web/utils/upstash/deep-clean-categorization.ts
  • apps/web/utils/email.ts
  • apps/web/app/(app)/[emailAccountId]/deep-clean/CreateCategoryButton.tsx
  • apps/web/utils/upstash/bulk-operations.ts
  • apps/web/hooks/useDeepClean.ts
  • apps/web/components/SideNav.tsx
  • apps/web/app/(app)/[emailAccountId]/deep-clean/Uncategorized.tsx
  • apps/web/app/api/webhooks/deep-clean/archive/route.ts
  • apps/web/app/api/user/deep-clean/senders/route.ts
  • apps/web/components/DeepCleanGroupedTable.tsx
  • apps/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.tsx
  • apps/web/app/(app)/[emailAccountId]/deep-clean/DeepCleanContent.tsx
  • apps/web/app/(app)/[emailAccountId]/deep-clean/CategorizeWithAiButton.tsx
  • apps/web/app/(app)/[emailAccountId]/deep-clean/page.tsx
  • apps/web/app/(app)/[emailAccountId]/deep-clean/BulkOperationProgress.tsx
  • apps/web/app/(app)/[emailAccountId]/deep-clean/CreateCategoryButton.tsx
  • apps/web/components/SideNav.tsx
  • apps/web/app/(app)/[emailAccountId]/deep-clean/Uncategorized.tsx
  • apps/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.tsx
  • apps/web/app/(app)/[emailAccountId]/deep-clean/DeepCleanContent.tsx
  • apps/web/app/(app)/[emailAccountId]/deep-clean/CategorizeWithAiButton.tsx
  • apps/web/app/(app)/[emailAccountId]/deep-clean/page.tsx
  • apps/web/app/(app)/[emailAccountId]/deep-clean/BulkOperationProgress.tsx
  • apps/web/app/(app)/[emailAccountId]/deep-clean/CreateCategoryButton.tsx
  • apps/web/components/SideNav.tsx
  • apps/web/app/(app)/[emailAccountId]/deep-clean/Uncategorized.tsx
  • apps/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/ai/categorize-sender/ai-categorize-senders.ts
  • apps/web/utils/email.test.ts
  • apps/web/app/api/user/categorize/senders/batch/handle-batch.ts
  • apps/web/utils/actions/deep-clean.ts
  • apps/web/utils/categorize/senders/categorize.ts
  • apps/web/utils/priority.ts
  • apps/web/app/api/user/deep-clean/progress/route.ts
  • apps/web/app/api/webhooks/deep-clean/mark-read/route.ts
  • apps/web/utils/redis/bulk-operation-progress.ts
  • apps/web/utils/upstash/deep-clean-categorization.ts
  • apps/web/utils/email.ts
  • apps/web/utils/upstash/bulk-operations.ts
  • apps/web/hooks/useDeepClean.ts
  • apps/web/app/api/webhooks/deep-clean/archive/route.ts
  • apps/web/app/api/user/deep-clean/senders/route.ts
  • apps/web/utils/ai/categorize-sender/ai-categorize-single-sender.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-senders.ts
  • apps/web/utils/email.test.ts
  • apps/web/utils/actions/deep-clean.ts
  • apps/web/utils/categorize/senders/categorize.ts
  • apps/web/utils/priority.ts
  • apps/web/utils/redis/bulk-operation-progress.ts
  • apps/web/utils/upstash/deep-clean-categorization.ts
  • apps/web/utils/email.ts
  • apps/web/utils/upstash/bulk-operations.ts
  • apps/web/utils/ai/categorize-sender/ai-categorize-single-sender.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-senders.ts
  • apps/web/utils/email.test.ts
  • apps/web/utils/actions/deep-clean.ts
  • apps/web/utils/categorize/senders/categorize.ts
  • apps/web/utils/priority.ts
  • apps/web/utils/redis/bulk-operation-progress.ts
  • apps/web/utils/upstash/deep-clean-categorization.ts
  • apps/web/utils/email.ts
  • apps/web/utils/upstash/bulk-operations.ts
  • apps/web/utils/ai/categorize-sender/ai-categorize-single-sender.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-senders.ts
  • apps/web/utils/ai/categorize-sender/ai-categorize-single-sender.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-senders.ts
  • apps/web/utils/ai/categorize-sender/ai-categorize-single-sender.ts
apps/web/prisma/schema.prisma

📄 CodeRabbit inference engine (.cursor/rules/prisma.mdc)

The Prisma schema file must be located at apps/web/prisma/schema.prisma

Files:

  • 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 the server-only module with vi.mock("server-only", () => ({}));
When testing code that uses Prisma, mock it with vi.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() in beforeEach)
Avoid testing implementation details; focus on observable behavior
Do not mock the Logger

Files:

  • apps/web/utils/email.test.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
  • apps/web/app/api/user/deep-clean/progress/route.ts
  • apps/web/app/api/webhooks/deep-clean/mark-read/route.ts
  • apps/web/app/api/webhooks/deep-clean/archive/route.ts
  • apps/web/app/api/user/deep-clean/senders/route.ts
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-action provides centralized error handling
Use Zod schemas for validation on both client and server
Use revalidatePath in server actions for cache invalidation

apps/web/utils/actions/**/*.ts: Use server actions (with next-safe-action) for all mutations (create/update/delete operations); do NOT use POST API routes for mutations.
Use revalidatePath in server actions to invalidate cache after mutations.

Files:

  • apps/web/utils/actions/deep-clean.ts
apps/web/utils/actions/*.ts

📄 CodeRabbit inference engine (.cursor/rules/server-actions.mdc)

apps/web/utils/actions/*.ts: Implement all server actions using the next-safe-action library for type safety, input validation, context management, and error handling. Refer to apps/web/utils/actions/safe-action.ts for client definitions (actionClient, actionClientUser, adminActionClient).
Use actionClientUser when only authenticated user context (userId) is needed.
Use actionClient when both authenticated user context and a specific emailAccountId are needed. The emailAccountId must be bound when calling the action from the client.
Use adminActionClient for actions restricted to admin users.
Access necessary context (like userId, emailAccountId, etc.) provided by the safe action client via the ctx object 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.
Use SafeError for expected/handled errors within actions if needed. next-safe-action provides centralized error handling.
Use the .metadata({ name: "actionName" }) method to provide a meaningful name for monitoring. Sentry instrumentation is automatically applied via withServerActionInstrumentation within the safe action clients.
If an action modifies data displayed elsewhere, use revalidatePath or revalidateTag from next/cache within the action handler as needed.

Server action files must start with use server

Files:

  • apps/web/utils/actions/deep-clean.ts
apps/web/app/api/**/route.ts

📄 CodeRabbit inference engine (apps/web/CLAUDE.md)

apps/web/app/api/**/route.ts: Use withAuth for user-level operations
Use withEmailAccount for 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 with withAuth or withEmailAccount middleware 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.ts
  • apps/web/app/api/webhooks/deep-clean/mark-read/route.ts
  • apps/web/app/api/webhooks/deep-clean/archive/route.ts
  • apps/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).
Use withEmailAccount middleware for API routes that operate on a specific email account (i.e., use or require emailAccountId).
Use withAuth middleware for API routes that operate at the user level (i.e., use or require only userId).
Use withError middleware (with proper validation) for public endpoints, custom authentication, or cron endpoints.
Cron endpoints MUST use withError middleware and validate the cron secret using hasCronSecret(request) or hasPostCronSecret(request).
Cron endpoints MUST capture unauthorized attempts with captureException and 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.
All findUnique and findFirst Prisma calls in API routes MUST include ownership filters (e.g., userId or emailAccountId).
All findMany Prisma 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.ts
  • apps/web/app/api/webhooks/deep-clean/mark-read/route.ts
  • apps/web/app/api/webhooks/deep-clean/archive/route.ts
  • apps/web/app/api/user/deep-clean/senders/route.ts
apps/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.
Call mutate() after successful mutations to refresh SWR data on the client.

Files:

  • apps/web/hooks/useDeepClean.ts
apps/web/hooks/**/*.{ts,tsx}

📄 CodeRabbit inference engine (apps/web/CLAUDE.md)

Call mutate() after successful mutations to refresh data

Files:

  • apps/web/hooks/useDeepClean.ts
apps/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.ts
apps/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 the use prefix (e.g., useAccounts.ts).
For fetching data from API endpoints in custom hooks, prefer using useSWR.
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.ts
apps/web/components/**/*.tsx

📄 CodeRabbit inference engine (apps/web/CLAUDE.md)

Use React Hook Form with Zod validation for form handling

Use the LoadingContent component 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.tsx
  • apps/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 caching

Applied 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` package

Applied 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

Comment on lines +33 to +38
// 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);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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.

Suggested change
// 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.

Comment on lines +37 to +66
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}`;
},
},
);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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.

Suggested change
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";
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

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.

Suggested change
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.

Comment on lines +38 to +41
} catch (error) {
toast.error("Failed to categorize senders");
console.error("Categorize more error:", error);
}
Copy link
Contributor

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.

Comment on lines +137 to +155
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,
});
}}
/>
);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +18 to +187
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 },
);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

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

Comment on lines +18 to +187
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 },
);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

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.

Comment on lines +627 to +640
<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">
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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.

Comment on lines 40 to 100
// 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,
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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

📥 Commits

Reviewing files that changed from the base of the PR and between d0d4645 and d7e3464.

📒 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}: Use createScopedLogger for logging in backend TypeScript files
Typically add the logger initialization at the top of the file when using createScopedLogger
Only use .with() on a logger instance within a specific function, not for a global logger

Import 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 use elements 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 type for 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 priority parameter 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 null when not provided
  • Applied consistently in both update and create paths

44-44: No issues found. Code is properly typed and safe.

The aiResult object returned from aiCategorizeSender is validated by a Zod schema that explicitly defines priority as z.enum(["low", "medium", "high"]). This ensures the priority field exists and is constrained to valid values. The updateSenderCategory function correctly accepts an optional priority parameter of type PriorityLevel, 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.

Comment on lines +164 to 175
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 };
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify behavioral change: getCategories now returns defaults only.

This is a significant change:

  1. Function no longer fetches user-specific categories from the database
  2. Always returns default categories filtered by enabled
  3. Uses category.name as id (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 getUserCategories instead

🏁 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 ts

Length 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 10

Length 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 2

Length 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 3

Length 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 -40

Length 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 2

Length 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 with id: category.name (string like "Newsletter", "Unknown")
  • updateSenderCategory() expects id to be an actual database primary key (UUID)
  • Lines 110 and 116 of updateSenderCategory() save categoryId: category.id directly 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:

  1. Generate proper UUIDs for default categories (consistent with database IDs), or
  2. Return categories without an id field (matching categorizeWithAi expectations), and let updateSenderCategory() handle category lookup differently

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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:

  1. Opening a separate issue/task to track it
  2. Removing the commented code (version control preserves the history)
  3. 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 inputs

Instructions 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 values

Return 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

📥 Commits

Reviewing files that changed from the base of the PR and between 9d5dca9 and 560d17b.

📒 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.ts
  • apps/web/__tests__/ai-categorize-senders.test.ts
  • apps/web/app/api/user/categorize/senders/batch/handle-batch.ts
  • apps/web/utils/ai/categorize-sender/prompts.ts
  • apps/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.ts
  • apps/web/__tests__/ai-categorize-senders.test.ts
  • apps/web/app/api/user/categorize/senders/batch/handle-batch.ts
  • apps/web/utils/ai/categorize-sender/prompts.ts
  • apps/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.ts
  • apps/web/__tests__/ai-categorize-senders.test.ts
  • apps/web/app/api/user/categorize/senders/batch/handle-batch.ts
  • apps/web/utils/ai/categorize-sender/prompts.ts
  • apps/web/utils/ai/categorize-sender/ai-categorize-senders.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/logging.mdc)

**/*.{ts,tsx}: Use createScopedLogger for logging in backend TypeScript files
Typically add the logger initialization at the top of the file when using createScopedLogger
Only use .with() on a logger instance within a specific function, not for a global logger

Import 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.ts
  • apps/web/__tests__/ai-categorize-senders.test.ts
  • apps/web/app/api/user/categorize/senders/batch/handle-batch.ts
  • apps/web/utils/ai/categorize-sender/prompts.ts
  • apps/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.ts
  • apps/web/utils/ai/categorize-sender/prompts.ts
  • apps/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.ts
  • apps/web/utils/ai/categorize-sender/prompts.ts
  • apps/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 use elements 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.ts
  • apps/web/__tests__/ai-categorize-senders.test.ts
  • apps/web/app/api/user/categorize/senders/batch/handle-batch.ts
  • apps/web/utils/ai/categorize-sender/prompts.ts
  • apps/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.ts
  • apps/web/__tests__/ai-categorize-senders.test.ts
  • apps/web/app/api/user/categorize/senders/batch/handle-batch.ts
  • apps/web/utils/ai/categorize-sender/prompts.ts
  • apps/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.ts
  • apps/web/utils/ai/categorize-sender/prompts.ts
  • apps/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.ts
  • apps/web/utils/ai/categorize-sender/prompts.ts
  • apps/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 the server-only module with vi.mock("server-only", () => ({}));
When testing code that uses Prisma, mock it with vi.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() in beforeEach)
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 result includes priority as 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 usage

Import aligns with the new centralized bulk API and respects the "@/” alias. LGTM.


135-135: Assert Unknown via direct equality — correct

Switching 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-off

Confirm 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 consolidation

Importing shared system/instructions/schema keeps flows consistent. LGTM.


16-17: Schema: align with bulk item shape — good

Validates LLM output at the array item level. LGTM.

apps/web/utils/ai/categorize-sender/prompts.ts (2)

3-30: Prompts centralization and priority guidance — solid

Clear system/instructions; priority enum in schema is helpful. LGTM.


31-44: Remove this review comment—zod 3.25.46 is a valid, stable release

The 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.

Comment on lines +46 to 47
const categoriesResult = await getCategories();
const { categories } = categoriesResult;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 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:

  1. Is this breaking change intentional and documented?
  2. Have existing users with custom categories been migrated or notified?
  3. 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 -A5

Length 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 -20

Length 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 -A10

Length 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 -80

Length 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",
Copy link
Contributor

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.

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a 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 &lt;&quot;quoted-name&quot;@domain.com&gt;, 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 &quot;Other&quot; 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">
&quot;Mark as Read&quot; 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 &quot;Uncategorized&quot; 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];
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Oct 17, 2025

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 &lt;&quot;quoted-name&quot;@domain.com&gt;, 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: &quot;quoted-name&quot;@domain.com or &#39;quoted-name&#39;@domain.com
+    const quotedNameMatch = emailPart.match(/^[&quot;&#39;]([^&quot;&#39;]+)[&quot;&#39;]@/);
+    if (quotedNameMatch) {
+      return quotedNameMatch[1];
+    }
+
</file context>
Suggested change
return quotedNameMatch[1];
return emailPart.replace(/^(["'])([^"']+)\1@/, "$2@");
Fix with Cubic

name:
FRONTEND_TO_BACKEND_CATEGORY[
category as keyof typeof FRONTEND_TO_BACKEND_CATEGORY
] || undefined,
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Oct 17, 2025

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 &quot;Other&quot; 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" });
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Oct 17, 2025

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>&quot;Mark as Read&quot; actions merely show a toast and never trigger the actual mark-as-read logic.</comment>

<file context>
@@ -0,0 +1,679 @@
+              &lt;DropdownMenuItem
+                onClick={() =&gt; {
+                  // TODO: Implement mark as read functionality
+                  toastSuccess({ description: &quot;Marked as read&quot; });
+                }}
+              &gt;
</file context>

✅ Addressed in 9f182e9


// Hide completed operations after 5 seconds
if (isCompleted) {
setTimeout(() => {
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Oct 17, 2025

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(() =&gt; {
+            // This will be handled by React&#39;s re-render when the operation is removed from Redis
+          }, 5000);
</file context>
Fix with Cubic

const { emailAccount } = userResult;

const categoriesResult = await getCategories({ emailAccountId });
const categoriesResult = await getCategories();
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Oct 17, 2025

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>
Fix with Cubic

updatedProgress.completedItems + updatedProgress.failedItems >=
updatedProgress.totalItems
) {
updatedProgress.status = "completed";
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Oct 17, 2025

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 &gt;=
+    updatedProgress.totalItems
+  ) {
+    updatedProgress.status = &quot;completed&quot;;
+  }
+
</file context>
Fix with Cubic


const category = categoryMap[categoryName];

if (!category) {
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Oct 17, 2025

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 &quot;Uncategorized&quot; 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>
Fix with Cubic

name="autoCategorizeSenders"
label="Auto categorize"
enabled={autoCategorizeSenders}
onChange={async (enabled) => {
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Oct 17, 2025

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=&quot;autoCategorizeSenders&quot;
+      label=&quot;Auto categorize&quot;
+      enabled={autoCategorizeSenders}
+      onChange={async (enabled) =&gt; {
+        await setAutoCategorizeAction(emailAccountId, {
+          autoCategorizeSenders: enabled,
</file context>
Fix with Cubic

await bulkCategorizeSendersAction(emailAccountId);

if (result?.serverError) {
setIsCategorizing(false);
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Oct 17, 2025

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>
Fix with Cubic


// If first-time user with few/no senders, automatically fetch and categorize
if (data.senders.length < 5) {
hasAutoTriggered.current = true;
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Oct 17, 2025

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 &lt; 5) {
+      hasAutoTriggered.current = true;
+      handleCategorizeMore();
+    }
</file context>
Fix with Cubic

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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 groupedEmails logic (lines 80-95) creates an "Uncategorized" group, but categoryMap only contains entries from the categories prop. When this guard returns null for "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 GroupRow call 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.id for both parameters prevents Gmail users from opening the correct message. Use firstMessage.id (defined on line 683) for the messageId prop.

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-clean module, not smart-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 the console.error statement.

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 handleCategorizeMore runs means the auto-trigger never fires if emailAccount loads after the first effect run. The early-return in handleCategorizeMore (line 24) prevents execution, but the flag locks out future attempts.

Move the flag inside handleCategorizeMore after 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 handleCategorizeMore to 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 category but 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

📥 Commits

Reviewing files that changed from the base of the PR and between 560d17b and 9f182e9.

📒 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.ts
  • apps/web/components/ProgressPanel.tsx
  • apps/web/app/(app)/[emailAccountId]/deep-clean/DeepCleanContent.tsx
  • apps/web/utils/actions/deep-clean.ts
  • apps/web/app/(app)/[emailAccountId]/deep-clean/page.tsx
  • apps/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-action provides centralized error handling
Use Zod schemas for validation on both client and server
Use revalidatePath in server actions for cache invalidation

apps/web/utils/actions/**/*.ts: Use server actions (with next-safe-action) for all mutations (create/update/delete operations); do NOT use POST API routes for mutations.
Use revalidatePath in server actions to invalidate cache after mutations.

Files:

  • apps/web/utils/actions/deep-clean.validation.ts
  • apps/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.ts
  • apps/web/components/ProgressPanel.tsx
  • apps/web/app/(app)/[emailAccountId]/deep-clean/DeepCleanContent.tsx
  • apps/web/utils/actions/deep-clean.ts
  • apps/web/app/(app)/[emailAccountId]/deep-clean/page.tsx
  • apps/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.ts
  • apps/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.ts file. These schemas are used by next-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}: Use createScopedLogger for logging in backend TypeScript files
Typically add the logger initialization at the top of the file when using createScopedLogger
Only use .with() on a logger instance within a specific function, not for a global logger

Import 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.ts
  • apps/web/components/ProgressPanel.tsx
  • apps/web/app/(app)/[emailAccountId]/deep-clean/DeepCleanContent.tsx
  • apps/web/utils/actions/deep-clean.ts
  • apps/web/app/(app)/[emailAccountId]/deep-clean/page.tsx
  • apps/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 the next-safe-action library for type safety, input validation, context management, and error handling. Refer to apps/web/utils/actions/safe-action.ts for client definitions (actionClient, actionClientUser, adminActionClient).
Use actionClientUser when only authenticated user context (userId) is needed.
Use actionClient when both authenticated user context and a specific emailAccountId are needed. The emailAccountId must be bound when calling the action from the client.
Use adminActionClient for actions restricted to admin users.
Access necessary context (like userId, emailAccountId, etc.) provided by the safe action client via the ctx object 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.
Use SafeError for expected/handled errors within actions if needed. next-safe-action provides centralized error handling.
Use the .metadata({ name: "actionName" }) method to provide a meaningful name for monitoring. Sentry instrumentation is automatically applied via withServerActionInstrumentation within the safe action clients.
If an action modifies data displayed elsewhere, use revalidatePath or revalidateTag from next/cache within the action handler as needed.

Server action files must start with use server

Files:

  • apps/web/utils/actions/deep-clean.validation.ts
  • apps/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.ts
  • apps/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.ts
  • apps/web/utils/actions/deep-clean.ts
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/ultracite.mdc)

**/*.{js,jsx,ts,tsx}: Don't use elements 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.ts
  • apps/web/components/ProgressPanel.tsx
  • apps/web/app/(app)/[emailAccountId]/deep-clean/DeepCleanContent.tsx
  • apps/web/utils/actions/deep-clean.ts
  • apps/web/app/(app)/[emailAccountId]/deep-clean/page.tsx
  • apps/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.ts
  • apps/web/components/ProgressPanel.tsx
  • apps/web/app/(app)/[emailAccountId]/deep-clean/DeepCleanContent.tsx
  • apps/web/utils/actions/deep-clean.ts
  • apps/web/app/(app)/[emailAccountId]/deep-clean/page.tsx
  • apps/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
Use result?.serverError with toastError and toastSuccess
Use LoadingContent component to handle loading and error states consistently
Pass loading, error, and children props to LoadingContent

Files:

  • apps/web/components/ProgressPanel.tsx
  • apps/web/app/(app)/[emailAccountId]/deep-clean/DeepCleanContent.tsx
  • apps/web/app/(app)/[emailAccountId]/deep-clean/page.tsx
  • apps/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 LoadingContent component 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.tsx
  • apps/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.tsx
  • apps/web/app/(app)/[emailAccountId]/deep-clean/DeepCleanContent.tsx
  • apps/web/app/(app)/[emailAccountId]/deep-clean/page.tsx
  • apps/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.tsx
  • apps/web/app/(app)/[emailAccountId]/deep-clean/DeepCleanContent.tsx
  • apps/web/app/(app)/[emailAccountId]/deep-clean/page.tsx
  • apps/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.tsx
  • apps/web/app/(app)/[emailAccountId]/deep-clean/DeepCleanContent.tsx
  • apps/web/app/(app)/[emailAccountId]/deep-clean/page.tsx
  • apps/web/components/DeepCleanGroupedTable.tsx
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/DeepCleanContent.tsx
  • apps/web/app/(app)/[emailAccountId]/deep-clean/page.tsx
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/DeepCleanContent.tsx
  • apps/web/app/(app)/[emailAccountId]/deep-clean/page.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/DeepCleanContent.tsx
  • apps/web/app/(app)/[emailAccountId]/deep-clean/page.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/DeepCleanContent.tsx
  • apps/web/app/(app)/[emailAccountId]/deep-clean/page.tsx
apps/web/app/**/*.tsx

📄 CodeRabbit inference engine (.cursor/rules/project-structure.mdc)

Components with onClick must be client components with use client directive

Files:

  • apps/web/app/(app)/[emailAccountId]/deep-clean/DeepCleanContent.tsx
  • apps/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 server

Applied 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 Zod

Applied 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 failures

Applied 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.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)/*/** : Components for the page are either put in page.tsx, or in the apps/web/app/(app)/PAGE_NAME folder

Applied 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.

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a 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 &quot;failed&quot; 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);
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Oct 17, 2025

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) =&gt; s.from);
+
+        logger.info(&quot;Added senders from EmailMessage&quot;, {
</file context>
Suggested change
sendersToCategorize = newSenders.map((s) => s.from);
sendersToCategorize = Array.from(new Set([...sendersToCategorize, ...newSenders.map((s) => s.from)]));
Fix with Cubic

toast.promise(
async () => {
setIsCategorizing(true);
setIsBulkCategorizing(true);
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Oct 17, 2025

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 () =&gt; {
+                setIsCategorizing(true);
+                setIsBulkCategorizing(true);
+                const result =
+                  await bulkCategorizeSendersAction(emailAccountId);
</file context>
Fix with Cubic

} = useForm<CreateCategoryBody>({
resolver: zodResolver(createCategoryBody),
defaultValues: {
id: category?.id,
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Oct 17, 2025

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&lt;CreateCategoryBody&gt;({
+    resolver: zodResolver(createCategoryBody),
+    defaultValues: {
+      id: category?.id,
+      name: category?.name,
+      description: category?.description,
</file context>
Fix with Cubic

emailAccountId: string;
}): Promise<Array<RedisBulkOperationProgress & { operationId: string }>> {
const pattern = `bulk-operation-progress:${emailAccountId}:*`;
const keys = await redis.keys(pattern);
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Oct 17, 2025

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&lt;Array&lt;RedisBulkOperationProgress &amp; { operationId: string }&gt;&gt; {
+  const pattern = `bulk-operation-progress:${emailAccountId}:*`;
+  const keys = await redis.keys(pattern);
+
+  if (keys.length === 0) return [];
</file context>
Fix with Cubic

updatedProgress.completedItems + updatedProgress.failedItems >=
updatedProgress.totalItems
) {
updatedProgress.status = "completed";
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Oct 17, 2025

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 &quot;failed&quot; 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 &gt;=
+    updatedProgress.totalItems
+  ) {
+    updatedProgress.status = &quot;completed&quot;;
+  }
+
</file context>
Fix with Cubic


// Hide completed operations after 5 seconds
if (isCompleted) {
setTimeout(() => {
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Oct 17, 2025

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(() =&gt; {
+            // This will be handled by React&#39;s re-render when the operation is removed from Redis
+          }, 5000);
</file context>
Fix with Cubic

: []),
{
name: "Deep Clean",
href: prefixPath(currentEmailAccountId, "/deep-clean"),
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Oct 17, 2025

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 = () =&gt; {
-        : []),
+      {
+        name: &quot;Deep Clean&quot;,
+        href: prefixPath(currentEmailAccountId, &quot;/deep-clean&quot;),
+        icon: BrushIcon,
+      },
</file context>
Fix with Cubic

export type BulkSendersSchema = z.infer<typeof bulkSendersSchema>;

export const categorizeMoreSendersSchema = z.object({
limit: z.number().optional().default(100),
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Oct 17, 2025

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&lt;typeof bulkSendersSchema&gt;;
+
+export const categorizeMoreSendersSchema = z.object({
+  limit: z.number().optional().default(100),
+});
+export type CategorizeMoreSendersSchema = z.infer&lt;
</file context>
Suggested change
limit: z.number().optional().default(100),
limit: z.number().int().positive().optional().default(100),
Fix with Cubic

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant