Repository: dcodesdev/LetterSpace
Pull request #15: Stats data fixed, Default cleanup interval to 90 days
Author: login: dcodesdev
Created: 2025-05-10T10:00:39Z | Merged: 2025-05-10T10:19:59Z



<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Added utility functions for querying messages by status, campaign, and organization.
  - Introduced a dashboard update notifier displaying the latest available version.

- **Bug Fixes**
  - Prevented division by zero errors in dashboard statistics and campaign delivery rate calculations.

- **Refactor**
  - Adjusted message cleanup logic to only delete messages with specific statuses.
  - Improved filtering and date handling in campaign and message statistics.

- **Chores**
  - Updated default cleanup interval from 7 to 90 days across backend, database, and settings UI.
  - Modified configuration files to enhance deployment behavior and image versioning.

- **Documentation**
  - Updated Docker Compose example to use the latest backend image tag.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/apps/backend/prisma/migrations/20250509113341_cleanup_interval_to_90_days/migration.sql b/apps/backend/prisma/migrations/20250509113341_cleanup_interval_to_90_days/migration.sql
new file mode 100644
index 0000000..6f076ec
--- /dev/null
+++ b/apps/backend/prisma/migrations/20250509113341_cleanup_interval_to_90_days/migration.sql
@@ -0,0 +1,2 @@
+-- AlterTable
+ALTER TABLE "GeneralSettings" ALTER COLUMN "cleanupInterval" SET DEFAULT 90;
diff --git a/apps/backend/prisma/schema.prisma b/apps/backend/prisma/schema.prisma
index 871b756..055d162 100644
--- a/apps/backend/prisma/schema.prisma
+++ b/apps/backend/prisma/schema.prisma
@@ -300,7 +300,7 @@ model GeneralSettings {
   defaultFromEmail String?
   defaultFromName  String?
   baseURL          String?
-  cleanupInterval  Int     @default(7)
+  cleanupInterval  Int     @default(90)
 
   organizationId String       @unique
   Organization   Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
diff --git a/apps/backend/src/cron/dailyMaintenance.ts b/apps/backend/src/cron/dailyMaintenance.ts
index 41b6dfe..9c7ae81 100644
--- a/apps/backend/src/cron/dailyMaintenance.ts
+++ b/apps/backend/src/cron/dailyMaintenance.ts
@@ -23,6 +23,9 @@ export const dailyMaintenanceCron = cronJob("daily-maintenance", async () => {
           Campaign: {
             organizationId: org.id,
           },
+          status: {
+            in: ["SENT", "OPENED", "CLICKED", "FAILED"],
+          },
           createdAt: {
             lt: cleanupOlderThanDate,
           },
diff --git a/apps/backend/src/dashboard/query.ts b/apps/backend/src/dashboard/query.ts
index 4e088f2..a622727 100644
--- a/apps/backend/src/dashboard/query.ts
+++ b/apps/backend/src/dashboard/query.ts
@@ -112,7 +112,7 @@ export const getDashboardStats = authProcedure
         title: campaign.title,
         status: campaign.status,
         completedAt: campaign.completedAt,
-        deliveryRate: (deliveredCount / totalCount) * 100,
+        deliveryRate: totalCount > 0 ? (deliveredCount / totalCount) * 100 : 0,
         totalMessages: totalCount,
         sentMessages: deliveredCount,
         createdAt: campaign.createdAt,
diff --git a/apps/backend/src/message/utils.ts b/apps/backend/src/message/utils.ts
new file mode 100644
index 0000000..ffa8f89
--- /dev/null
+++ b/apps/backend/src/message/utils.ts
@@ -0,0 +1,84 @@
+import { prisma } from "../utils/prisma"
+import { MessageStatus } from "../../prisma/client"
+
+interface MessageQueryOptions {
+  campaignId?: string
+  organizationId: string
+  status: MessageStatus | MessageStatus[]
+}
+
+export async function findMessagesByStatus({
+  campaignId,
+  organizationId,
+  status,
+}: MessageQueryOptions) {
+  return prisma.message.findMany({
+    where: {
+      ...(campaignId && { campaignId }),
+      Campaign: {
+        organizationId,
+      },
+      status: Array.isArray(status) ? { in: status } : status,
+    },
+  })
+}
+
+interface CampaignMessagesQueryOptions {
+  campaignId: string
+  organizationId: string
+}
+
+export async function getDeliveredMessages({
+  campaignId,
+  organizationId,
+}: CampaignMessagesQueryOptions) {
+  return findMessagesByStatus({
+    campaignId,
+    organizationId,
+    status: ["SENT", "CLICKED", "OPENED"],
+  })
+}
+
+export async function getFailedMessages({
+  campaignId,
+  organizationId,
+}: CampaignMessagesQueryOptions) {
+  return findMessagesByStatus({
+    campaignId,
+    organizationId,
+    status: "FAILED",
+  })
+}
+
+export async function getOpenedMessages({
+  campaignId,
+  organizationId,
+}: CampaignMessagesQueryOptions) {
+  return findMessagesByStatus({
+    campaignId,
+    organizationId,
+    status: ["OPENED", "CLICKED"], // Clicked implies opened
+  })
+}
+
+export async function getClickedMessages({
+  campaignId,
+  organizationId,
+}: CampaignMessagesQueryOptions) {
+  return findMessagesByStatus({
+    campaignId,
+    organizationId,
+    status: "CLICKED",
+  })
+}
+
+export async function getQueuedMessages({
+  campaignId,
+  organizationId,
+}: CampaignMessagesQueryOptions) {
+  return findMessagesByStatus({
+    campaignId,
+    organizationId,
+    status: "QUEUED",
+  })
+}
diff --git a/apps/backend/src/stats/query.ts b/apps/backend/src/stats/query.ts
index 2700acc..fa715ba 100644
--- a/apps/backend/src/stats/query.ts
+++ b/apps/backend/src/stats/query.ts
@@ -8,6 +8,7 @@ import {
   countDistinctRecipients,
   countDistinctRecipientsInTimeRange,
 } from "../../prisma/client/sql"
+import { MessageStatus } from "../../prisma/client"
 
 export const getStats = authProcedure
   .input(
@@ -20,6 +21,13 @@ export const getStats = authProcedure
     const thirtyDaysAgo = subDays(now, 30)
     const sixtyDaysAgo = subDays(now, 60)
 
+    const processedMessageStatuses: MessageStatus[] = [
+      "SENT",
+      "CLICKED",
+      "OPENED",
+      "FAILED",
+    ]
+
     // Check auth
     const hasAccess = await prisma.userOrganization.findFirst({
       where: {
@@ -43,6 +51,7 @@ export const getStats = authProcedure
             Campaign: {
               organizationId: input.organizationId,
             },
+            status: { in: processedMessageStatuses },
           },
         }),
         prisma.message.count({
@@ -54,6 +63,7 @@ export const getStats = authProcedure
               gte: thirtyDaysAgo,
               lt: now,
             },
+            status: { in: processedMessageStatuses },
           },
         }),
         prisma.message.count({
@@ -65,6 +75,7 @@ export const getStats = authProcedure
               gte: sixtyDaysAgo,
               lt: thirtyDaysAgo,
             },
+            status: { in: processedMessageStatuses },
           },
         }),
       ])
@@ -177,12 +188,20 @@ export const getStats = authProcedure
         where: {
           organizationId: input.organizationId,
           status: "COMPLETED",
+          createdAt: {
+            gte: thirtyDaysAgo,
+            lt: now,
+          },
         },
       }),
       completedCampaignsLastMonth: prisma.campaign.count({
         where: {
           organizationId: input.organizationId,
           status: "COMPLETED",
+          createdAt: {
+            gte: sixtyDaysAgo,
+            lt: thirtyDaysAgo,
+          },
         },
       }),
       deliveryRateThisMonth: (async () => {
@@ -194,7 +213,7 @@ export const getStats = authProcedure
             Campaign: {
               organizationId: input.organizationId,
             },
-            createdAt: {
+            sentAt: {
               gte: thirtyDaysAgo,
               lt: now,
             },
@@ -215,7 +234,7 @@ export const getStats = authProcedure
             Campaign: {
               organizationId: input.organizationId,
             },
-            createdAt: {
+            sentAt: {
               gte: sixtyDaysAgo,
               lt: thirtyDaysAgo,
             },
@@ -234,7 +253,7 @@ export const getStats = authProcedure
             Campaign: {
               organizationId: input.organizationId,
             },
-            createdAt: {
+            sentAt: {
               gte: thirtyDaysAgo,
               lt: now,
             },
@@ -253,7 +272,7 @@ export const getStats = authProcedure
             Campaign: {
               organizationId: input.organizationId,
             },
-            createdAt: {
+            sentAt: {
               gte: sixtyDaysAgo,
               lt: thirtyDaysAgo,
             },
@@ -320,7 +339,7 @@ export const getStats = authProcedure
       },
       messages: {
         total: totalMessages,
-        Last30Days: totalMessagesLast30Days,
+        last30Days: totalMessagesLast30Days,
         lastPeriod: totalMessagesLastPeriod,
       },
       recipients: {
diff --git a/apps/docs/src/app/getting-started/page.mdx b/apps/docs/src/app/getting-started/page.mdx
index aa1a094..47f1cc9 100644
--- a/apps/docs/src/app/getting-started/page.mdx
+++ b/apps/docs/src/app/getting-started/page.mdx
@@ -29,7 +29,7 @@ We've made LetterSpace for simplicity and ease of use. The easiest way to get st
           - app_network
 
       backend:
-        image: ghcr.io/dcodesdev/letterspace:0
+        image: ghcr.io/dcodesdev/letterspace:latest
         pull_policy: always
         restart: always
         depends_on:
diff --git a/apps/docs/vercel.json b/apps/docs/vercel.json
index 415ed7b..a1c9973 100644
--- a/apps/docs/vercel.json
+++ b/apps/docs/vercel.json
@@ -1,3 +1,3 @@
 {
-  "ignoreCommand": "git diff --quiet HEAD^ HEAD ./"
+  "ignoreCommand": "git diff --quiet HEAD^ HEAD ./ && git diff --quiet HEAD^ HEAD ../../package.json"
 }
diff --git a/apps/landing-page/vercel.json b/apps/landing-page/vercel.json
index 415ed7b..a1c9973 100644
--- a/apps/landing-page/vercel.json
+++ b/apps/landing-page/vercel.json
@@ -1,3 +1,3 @@
 {
-  "ignoreCommand": "git diff --quiet HEAD^ HEAD ./"
+  "ignoreCommand": "git diff --quiet HEAD^ HEAD ./ && git diff --quiet HEAD^ HEAD ../../package.json"
 }
diff --git a/apps/web/package.json b/apps/web/package.json
index 67cc08b..f1676dd 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -29,6 +29,7 @@
     "react-router": "^7.5.3",
     "react-scan": "^0.1.4",
     "recharts": "^2.15.3",
+    "semver": "^7.7.1",
     "sonner": "^1.7.4",
     "superjson": "^2.2.2",
     "usehooks-ts": "^3.1.1",
@@ -41,6 +42,7 @@
     "@types/react": "^19.1.2",
     "@types/react-dom": "^19.1.3",
     "@types/react-router": "^5.1.20",
+    "@types/semver": "^7.7.0",
     "@vite-pwa/assets-generator": "^1.0.0",
     "@vitejs/plugin-react": "^4.4.1",
     "autoprefixer": "^10.4.21",
diff --git a/apps/web/src/hooks/index.ts b/apps/web/src/hooks/index.ts
index 960a9ee..e315cb0 100644
--- a/apps/web/src/hooks/index.ts
+++ b/apps/web/src/hooks/index.ts
@@ -5,3 +5,4 @@ export * from "./useUpdateEffect"
 export * from "./useIsMounted"
 export * from "./usePagination"
 export * from "./useQueryState"
+export * from "./use-update-check"
diff --git a/apps/web/src/hooks/use-update-check.ts b/apps/web/src/hooks/use-update-check.ts
new file mode 100644
index 0000000..0eff2b8
--- /dev/null
+++ b/apps/web/src/hooks/use-update-check.ts
@@ -0,0 +1,34 @@
+import { useQuery } from "@tanstack/react-query"
+import { APP_VERSION } from "@repo/shared"
+import semver from "semver"
+
+type UpdateInfo = {
+  hasUpdate: boolean
+  latestVersion: string
+}
+
+type GithubRelease = {
+  tag_name: string
+}
+
+async function fetchLatestRelease(): Promise<GithubRelease> {
+  const res = await fetch(
+    "https://api.github.com/repos/dcodesdev/LetterSpace/releases/latest"
+  )
+  if (!res.ok) throw new Error("Failed to fetch release")
+  return res.json()
+}
+
+export function useUpdateCheck(): UpdateInfo {
+  const { data } = useQuery({
+    queryKey: ["latest-release"],
+    queryFn: fetchLatestRelease,
+    staleTime: 1000 * 60 * 10,
+    retry: false,
+  })
+
+  const latestVersion = data?.tag_name?.replace("v", "") || APP_VERSION
+  const hasUpdate = semver.gt(latestVersion, APP_VERSION)
+
+  return { hasUpdate, latestVersion }
+}
diff --git a/apps/web/src/pages/dashboard/campaigns/[id]/stats.tsx b/apps/web/src/pages/dashboard/campaigns/[id]/stats.tsx
index d4e2fa8..14c068e 100644
--- a/apps/web/src/pages/dashboard/campaigns/[id]/stats.tsx
+++ b/apps/web/src/pages/dashboard/campaigns/[id]/stats.tsx
@@ -105,9 +105,11 @@ export const Stats = () => {
           <CardContent>
             <div className="space-y-2">
               <div className="text-2xl font-bold">
-                {(
-                  (campaign.stats.sentMessages / campaign.stats.totalMessages) *
-                  100
+                {(campaign.stats.totalMessages > 0
+                  ? (campaign.stats.sentMessages /
+                      campaign.stats.totalMessages) *
+                    100
+                  : 0
                 ).toFixed(1)}
                 %
               </div>
diff --git a/apps/web/src/pages/dashboard/layout.tsx b/apps/web/src/pages/dashboard/layout.tsx
index 0b259fc..6b01ab8 100644
--- a/apps/web/src/pages/dashboard/layout.tsx
+++ b/apps/web/src/pages/dashboard/layout.tsx
@@ -25,6 +25,7 @@ import {
   Building2,
   LogOut,
   LucideIcon,
+  ArrowUpCircle,
 } from "lucide-react"
 import { useSession } from "@/hooks"
 import { useLocation } from "react-router"
@@ -35,6 +36,7 @@ import {
   LetterSpaceText,
 } from "@/components"
 import { APP_VERSION } from "@repo/shared"
+import { useUpdateCheck } from "@/hooks/use-update-check"
 
 const sidebarItems = [
   {
@@ -116,6 +118,7 @@ function NavItem({
 export function DashboardLayout() {
   const { orgId, user, organization, logout } = useSession()
   const location = useLocation()
+  const { hasUpdate, latestVersion } = useUpdateCheck()
 
   // Helper function to check if a menu item is active
   const isActive = (itemUrl: string) => {
@@ -166,10 +169,6 @@ export function DashboardLayout() {
                         >
                           {item.title}
                         </NavItem>
-                        {/* <Link to={item.url}>
-                          <item.icon className="h-4 w-4" />
-                          <span>{item.title}</span>
-                        </Link> */}
                       </SidebarMenuButton>
                     </SidebarMenuItem>
                   ))}
@@ -210,6 +209,17 @@ export function DashboardLayout() {
                     v{APP_VERSION}
                   </span>
                 </WithTooltip>
+                {hasUpdate && (
+                  <a
+                    href="https://github.com/dcodesdev/LetterSpace/releases/latest"
+                    target="_blank"
+                    rel="noopener noreferrer"
+                    className="flex items-center gap-1 text-xs text-emerald-600 hover:text-emerald-500 transition-colors"
+                  >
+                    <ArrowUpCircle className="h-3 w-3" />
+                    <span>Update available v{latestVersion}</span>
+                  </a>
+                )}
               </div>
             </SidebarMenu>
           </SidebarFooter>
diff --git a/apps/web/src/pages/dashboard/settings/general-settings.tsx b/apps/web/src/pages/dashboard/settings/general-settings.tsx
index 9f90bef..e105059 100644
--- a/apps/web/src/pages/dashboard/settings/general-settings.tsx
+++ b/apps/web/src/pages/dashboard/settings/general-settings.tsx
@@ -50,7 +50,7 @@ export function GeneralSettings() {
       defaultFromEmail: "",
       defaultFromName: "",
       baseURL: "",
-      cleanupInterval: 7,
+      cleanupInterval: 90,
     },
   })
 
@@ -62,7 +62,7 @@ export function GeneralSettings() {
         defaultFromEmail: settings.defaultFromEmail ?? "",
         defaultFromName: settings.defaultFromName ?? "",
         baseURL: settings.baseURL ?? "",
-        cleanupInterval: settings.cleanupInterval ?? 7,
+        cleanupInterval: settings.cleanupInterval ?? 90,
       })
     }
   }, [settings, form])
@@ -73,7 +73,7 @@ export function GeneralSettings() {
         defaultFromEmail: settings.defaultFromEmail ?? "",
         defaultFromName: settings.defaultFromName ?? "",
         baseURL: settings.baseURL ?? "",
-        cleanupInterval: settings.cleanupInterval ?? 7,
+        cleanupInterval: settings.cleanupInterval ?? 90,
       })
       utils.settings.getGeneral.invalidate()
     },
@@ -192,10 +192,10 @@ export function GeneralSettings() {
               control={form.control}
               name="cleanupInterval"
               label="Cleanup Interval (days)"
-              description="Number of days after which to cleanup old data (e.g. sent messages, logs). Default is 7 days."
+              description="Number of days after which to cleanup old data (e.g. sent messages, logs). Default is 90 days."
               inputProps={{
                 type: "number",
-                placeholder: "7",
+                placeholder: "90",
               }}
             />
           </form>
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 27910ed..855e109 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -308,6 +308,9 @@ importers:
       recharts:
         specifier: ^2.15.3
         version: 2.15.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+      semver:
+        specifier: ^7.7.1
+        version: 7.7.1
       sonner:
         specifier: ^1.7.4
         version: 1.7.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
@@ -339,6 +342,9 @@ importers:
       '@types/react-router':
         specifier: ^5.1.20
         version: 5.1.20
+      '@types/semver':
+        specifier: ^7.7.0
+        version: 7.7.0
       '@vite-pwa/assets-generator':
         specifier: ^1.0.0
         version: 1.0.0
@@ -3680,6 +3686,9 @@ packages:
   '@types/resolve@1.20.2':
     resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==}
 
+  '@types/semver@7.7.0':
+    resolution: {integrity: sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==}
+
   '@types/send@0.17.4':
     resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==}
 
@@ -7262,11 +7271,6 @@ packages:
     engines: {node: '>=10'}
     hasBin: true
 
-  semver@7.7.0:
-    resolution: {integrity: sha512-DrfFnPzblFmNrIZzg5RzHegbiRWg7KMR7btwi2yjHwx06zsUbO5g613sVwEV7FTwmzJu+Io0lJe2GJ3LxqpvBQ==}
-    engines: {node: '>=10'}
-    hasBin: true
-
   semver@7.7.1:
     resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==}
     engines: {node: '>=10'}
@@ -11321,6 +11325,8 @@ snapshots:
 
   '@types/resolve@1.20.2': {}
 
+  '@types/semver@7.7.0': {}
+
   '@types/send@0.17.4':
     dependencies:
       '@types/mime': 1.3.5
@@ -11467,7 +11473,7 @@ snapshots:
       fast-glob: 3.3.3
       is-glob: 4.0.3
       minimatch: 9.0.5
-      semver: 7.7.0
+      semver: 7.7.1
       ts-api-utils: 2.1.0(typescript@5.8.3)
       typescript: 5.8.3
     transitivePeerDependencies:
@@ -14050,7 +14056,7 @@ snapshots:
       lodash.isstring: 4.0.1
       lodash.once: 4.1.1
       ms: 2.1.3
-      semver: 7.7.0
+      semver: 7.7.1
 
   jsx-ast-utils@3.3.5:
     dependencies:
@@ -15938,8 +15944,6 @@ snapshots:
 
   semver@7.6.2: {}
 
-  semver@7.7.0: {}
-
   semver@7.7.1: {}
 
   send@0.19.0:


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 3**

<details>
<summary>🧹 Nitpick comments (3)</summary><blockquote>

<details>
<summary>apps/docs/src/app/getting-started/page.mdx (1)</summary><blockquote>

`39-43`: **Consider noting the new default cleanup interval.**

This PR bumps the default message cleanup interval from 7 to 90 days. It may be helpful to document this in the getting-started guide—either by mentioning the default behavior or by showing how to override it via an environment variable (e.g., `CLEANUP_INTERVAL_DAYS`).

</blockquote></details>
<details>
<summary>apps/backend/src/message/utils.ts (2)</summary><blockquote>

`10-24`: **Nice implementation of the core query function with flexible status filtering.**

The `findMessagesByStatus` function is well-designed:
- Properly handles both single status and array of statuses
- Conditionally includes campaignId only when provided
- Ensures organization-level data isolation through the Campaign relation

Consider adding pagination options for queries that might return large result sets.


```diff
interface MessageQueryOptions {
  campaignId?: string
  organizationId: string
  status: MessageStatus | MessageStatus[]
+ take?: number
+ skip?: number
}

export async function findMessagesByStatus({
  campaignId,
  organizationId,
  status,
+ take,
+ skip,
}: MessageQueryOptions) {
  return prisma.message.findMany({
    where: {
      ...(campaignId && { campaignId }),
      Campaign: {
        organizationId,
      },
      status: Array.isArray(status) ? { in: status } : status,
    },
+   ...(take !== undefined && { take }),
+   ...(skip !== undefined && { skip }),
  })
}
```

---

`64-84`: **Consistent implementation of remaining query functions.**

The `getClickedMessages` and `getQueuedMessages` functions follow the established pattern, making the API intuitive and maintainable.

Consider adding error handling to catch and properly report database-related errors across all these functions.


```diff
export async function findMessagesByStatus({
  campaignId,
  organizationId,
  status,
}: MessageQueryOptions) {
+  try {
    return prisma.message.findMany({
      where: {
        ...(campaignId && { campaignId }),
        Campaign: {
          organizationId,
        },
        status: Array.isArray(status) ? { in: status } : status,
      },
    })
+  } catch (error) {
+    console.error('Error querying messages by status:', error);
+    throw error;
+  }
}
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between baeb750799b08a951d5a67f683df11a60320deca and 353e6e2481fb2f1e38d7b5e47058495bec5f6f46.

</details>

<details>
<summary>📒 Files selected for processing (11)</summary>

* `apps/backend/prisma/migrations/20250509113341_cleanup_interval_to_90_days/migration.sql` (1 hunks)
* `apps/backend/prisma/schema.prisma` (1 hunks)
* `apps/backend/src/cron/dailyMaintenance.ts` (1 hunks)
* `apps/backend/src/dashboard/query.ts` (1 hunks)
* `apps/backend/src/message/utils.ts` (1 hunks)
* `apps/backend/src/stats/query.ts` (11 hunks)
* `apps/docs/src/app/getting-started/page.mdx` (1 hunks)
* `apps/docs/vercel.json` (1 hunks)
* `apps/landing-page/vercel.json` (1 hunks)
* `apps/web/src/pages/dashboard/campaigns/[id]/stats.tsx` (1 hunks)
* `apps/web/src/pages/dashboard/settings/general-settings.tsx` (4 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (2)</summary>

<details>
<summary>apps/backend/src/stats/query.ts (1)</summary><blockquote>

<details>
<summary>apps/backend/src/utils/prisma.ts (1)</summary>

* `prisma` (3-28)

</details>

</blockquote></details>
<details>
<summary>apps/backend/src/message/utils.ts (1)</summary><blockquote>

<details>
<summary>apps/backend/src/utils/prisma.ts (1)</summary>

* `prisma` (3-28)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>🔇 Additional comments (18)</summary><blockquote>

<details>
<summary>apps/docs/src/app/getting-started/page.mdx (1)</summary>

`32-32`: **Updated backend image to `latest`.**

The Docker Compose example now pulls `ghcr.io/dcodesdev/letterspace:latest`, ensuring users deploy the most recent backend build. This change aligns the docs with the updated release pipeline.

</details>
<details>
<summary>apps/docs/vercel.json (1)</summary>

`2-2`: **Good enhancement to the deployment condition**

The updated `ignoreCommand` now includes a check for changes in the root package.json file, ensuring the docs app is redeployed if there are changes that might affect it from the monorepo root. This is a sensible improvement to the deployment configuration.

</details>
<details>
<summary>apps/landing-page/vercel.json (1)</summary>

`2-2`: **Ensures consistent deployment behavior**

This change mirrors the update made to the docs app, creating consistency across Vercel configurations in the monorepo. Good improvement to ensure both apps respond similarly to changes in the root package.json file.

</details>
<details>
<summary>apps/backend/prisma/migrations/20250509113341_cleanup_interval_to_90_days/migration.sql (1)</summary>

`1-2`: **Appropriate migration for the increased cleanup interval**

This migration correctly updates the default value for the `cleanupInterval` column from 7 to 90 days, aligning with the PR objective to extend the cleanup period.

</details>
<details>
<summary>apps/backend/prisma/schema.prisma (1)</summary>

`303-303`: **Schema update matches the migration file**

The Prisma schema change correctly updates the default cleanup interval from 7 to 90 days, consistent with the migration file and PR objectives.

</details>
<details>
<summary>apps/backend/src/cron/dailyMaintenance.ts (1)</summary>

`26-28`: **Improved message cleanup logic with status filtering**

The addition of status filtering to the message deletion query is a significant improvement. Now, only messages with completed or failed states ("SENT", "OPENED", "CLICKED", "FAILED") will be deleted, preserving messages that may still be in processing states (like "QUEUED" or "PENDING").

This change works well with the extended cleanup interval and prevents accidental deletion of messages that are still being processed.

</details>
<details>
<summary>apps/web/src/pages/dashboard/settings/general-settings.tsx (4)</summary>

`53-53`: **Update of default cleanup interval from 7 to 90 days**

The default value for the cleanup interval has been increased from 7 to 90 days, which provides users with a more generous data retention period by default.

---

`65-65`: **Consistent update of cleanup interval in form reset**

The fallback value for cleanup interval in the form reset is properly updated to match the new 90-day default, maintaining consistency across the component.

---

`76-76`: **Consistent update of cleanup interval in mutation success handler**

The fallback value for cleanup interval in the mutation success handler is correctly updated to 90 days, ensuring consistency when settings are saved.

---

`195-195`: **Updated UI text to reflect new default cleanup interval**

The description and placeholder text for the cleanup interval input field have been updated to reflect the new 90-day default, providing users with accurate information.



Also applies to: 198-198

</details>
<details>
<summary>apps/backend/src/stats/query.ts (4)</summary>

`54-54`: **Standardized message status filtering**

The message filtering now consistently uses the defined `processedMessageStatuses` array instead of hardcoded values in each query, improving maintainability and ensuring consistent filtering criteria.



Also applies to: 66-66, 78-78

---

`191-195`: **Added date filters for completed campaigns**

Date range filters have been added to completed campaign queries, ensuring that statistics for "this month" and "last month" are correctly calculated based on when campaigns were created.



Also applies to: 201-205

---

`216-216`: **Improved date filtering using sentAt instead of createdAt**

The date filtering for message statistics now uses `sentAt` instead of `createdAt`, which is more accurate since it reflects when messages were actually sent rather than when they were created in the system.


This change will provide more accurate statistics for time-based metrics, as messages might be created at one time but actually sent later.


Also applies to: 237-237, 256-256, 275-275

---

`342-342`: **Renamed key for consistent camelCase naming**

The key `Last30Days` has been renamed to `last30Days` for consistency with JavaScript camelCase naming conventions.


This small change improves code style consistency across the codebase.

</details>
<details>
<summary>apps/backend/src/message/utils.ts (4)</summary>

`1-8`: **Well-structured message query interface.**

The interface definition is clean with appropriate optional/required fields and proper typing for message status. Good practice to use TypeScript interfaces for query parameters.

---

`26-40`: **Good separation of concerns with specialized query functions.**

The `getDeliveredMessages` function properly wraps the core query function with specific status values. Including "SENT", "CLICKED", and "OPENED" as delivered messages makes sense from a business perspective.

---

`42-51`: **Clean implementation for fetching failed messages.**

This wrapper function is straightforward and follows the same pattern as other utility functions.

---

`53-62`: **Well-documented business logic for opened messages.**

Good choice to include both "OPENED" and "CLICKED" statuses with a helpful comment explaining that clicked implies opened. This reflects a clear understanding of the message lifecycle.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-10T10:02:13Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>apps/web/src/hooks/use-update-check.ts (2)</summary><blockquote>

`1-33`: **Well-structured update check hook with good caching.**

The hook is well-implemented with React Query, using appropriate caching (10-minute stale time) to minimize API calls and proper type definitions. The error handling in the fetch function is appropriate.



Consider these optional improvements:

1. Add error state exposure to handle API failures gracefully in the UI
2. Implement semantic version comparison instead of string equality

```diff
- const hasUpdate = latestVersion !== APP_VERSION
+ // Use semver comparison (requires installing semver package)
+ // import * as semver from 'semver'
+ const hasUpdate = semver.gt(latestVersion, APP_VERSION)
```

---

`13-19`: **Consider parameterizing the repository URL.**

The hardcoded GitHub repository URL reduces reusability and complicates testing environments or forks.


```diff
- async function fetchLatestRelease(): Promise<GithubRelease> {
-   const res = await fetch(
-     "https://api.github.com/repos/dcodesdev/LetterSpace/releases/latest"
-   )
+ async function fetchLatestRelease(): Promise<GithubRelease> {
+   const REPO_OWNER = "dcodesdev"
+   const REPO_NAME = "LetterSpace"
+   const res = await fetch(
+     `https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/releases/latest`
+   )
```

Also consider handling GitHub API rate limiting with appropriate headers or by implementing exponential backoff.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 353e6e2481fb2f1e38d7b5e47058495bec5f6f46 and 18828a5700bf800d366752931370af3fc7ae99c7.

</details>

<details>
<summary>📒 Files selected for processing (3)</summary>

* `apps/web/src/hooks/index.ts` (1 hunks)
* `apps/web/src/hooks/use-update-check.ts` (1 hunks)
* `apps/web/src/pages/dashboard/layout.tsx` (4 hunks)

</details>

<details>
<summary>✅ Files skipped from review due to trivial changes (1)</summary>

* apps/web/src/hooks/index.ts

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (2)</summary>

<details>
<summary>apps/web/src/pages/dashboard/layout.tsx (1)</summary><blockquote>

<details>
<summary>apps/web/src/hooks/use-update-check.ts (1)</summary>

* `useUpdateCheck` (21-33)

</details>

</blockquote></details>
<details>
<summary>apps/web/src/hooks/use-update-check.ts (1)</summary><blockquote>

<details>
<summary>packages/shared/index.ts (1)</summary>

* `APP_VERSION` (3-3)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>🔇 Additional comments (2)</summary><blockquote>

<details>
<summary>apps/web/src/pages/dashboard/layout.tsx (2)</summary>

`121-121`: **Good implementation of the update check hook.**

The hook is properly used at the component level, adhering to React best practices. This implementation allows for efficient update checking without unnecessary re-renders.

---

`212-222`: **Well-designed update notification UI.**

The notification is:
- Conditionally rendered only when updates are available
- Styled distinctly with semantic colors for visibility
- Includes both an icon and version information
- Links directly to the GitHub releases page with proper security attributes

This provides a non-intrusive way to keep users informed about new versions.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-10T10:13:01Z

Review comments:
diff_hunk: @@ -105,9 +105,11 @@ export const Stats = () => {
           <CardContent>
             <div className="space-y-2">
               <div className="text-2xl font-bold">
-                {(
-                  (campaign.stats.sentMessages / campaign.stats.totalMessages) *
-                  100
+                {(campaign.stats.totalMessages > 0
+                  ? (campaign.stats.sentMessages /
+                      campaign.stats.totalMessages) *
+                    100
+                  : 0
                 ).toFixed(1)}
path: apps/web/src/pages/dashboard/campaigns/[id]/stats.tsx
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Added division by zero check for delivery status percentage**

This change adds protection against division by zero when calculating the percentage of sent messages. Previously, if `totalMessages` was zero, this could have caused a runtime error or NaN value.



The defensive programming approach prevents errors in edge cases where a campaign has no messages yet.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-10T10:02:12Z
author_association: NONE
diff_hunk: @@ -112,7 +112,7 @@ export const getDashboardStats = authProcedure
         title: campaign.title,
         status: campaign.status,
         completedAt: campaign.completedAt,
-        deliveryRate: (deliveredCount / totalCount) * 100,
+        deliveryRate: totalCount > 0 ? (deliveredCount / totalCount) * 100 : 0,
path: apps/backend/src/dashboard/query.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Added division by zero check for delivery rate calculation**

This change adds protection against division by zero when calculating the delivery rate for recent campaigns. This is a good defensive programming practice that prevents errors in edge cases where a campaign has no messages.



This fix aligns with a similar update in the frontend stats component, ensuring consistent handling of edge cases across the application.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-10T10:02:12Z
author_association: NONE
diff_hunk: @@ -8,6 +8,7 @@ import {
   countDistinctRecipients,
   countDistinctRecipientsInTimeRange,
 } from "../../prisma/client/sql"
+import { MessageStatus } from "../../prisma/client"
path: apps/backend/src/stats/query.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Added constant for processed message statuses**

A new constant `processedMessageStatuses` has been defined to standardize the message statuses that should be considered in various statistics calculations.


This is a good improvement for DRY code principles. Consider extracting this constant to a shared location if it's used in other files as well, to ensure consistent filtering across the application.


Also applies to: 24-29

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-10T10:02:12Z
author_association: NONE
diff_hunk: @@ -0,0 +1,33 @@
+import { useQuery } from "@tanstack/react-query"
+import { APP_VERSION } from "@repo/shared"
+
+type UpdateInfo = {
+  hasUpdate: boolean
+  latestVersion: string
+}
+
+type GithubRelease = {
+  tag_name: string
+}
+
+async function fetchLatestRelease(): Promise<GithubRelease> {
+  const res = await fetch(
+    "https://api.github.com/repos/dcodesdev/LetterSpace/releases/latest"
+  )
+  if (!res.ok) throw new Error("Failed to fetch release")
+  return res.json()
+}
+
+export function useUpdateCheck(): UpdateInfo {
+  const { data } = useQuery({
+    queryKey: ["latest-release"],
+    queryFn: fetchLatestRelease,
+    staleTime: 1000 * 60 * 10,
+    retry: false,
+  })
+
+  const latestVersion = data?.tag_name?.replace("v", "") || APP_VERSION
+  const hasUpdate = latestVersion !== APP_VERSION
path: apps/web/src/hooks/use-update-check.ts
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Improve version comparison logic.**

The current string comparison might not correctly handle semantic versioning (e.g., "1.10.0" vs "1.2.0" would incorrectly show 1.2.0 as newer).

---

🌐 Web query:

```web
What's the best way to compare semantic version strings in JavaScript?
```

💡 Result:

The best way to compare semantic version strings in JavaScript is to use a dedicated library designed for that purpose to handle all the intricacies of semantic versioning (semver) such as pre-release and build metadata. The most popular and reliable library for this is the `semver` package.

Here's an example of how to use the `semver` package:

1. First, install the package if you haven't already:

```bash
npm install semver
```

2. Then, you can use it in your JavaScript code:

```js
const semver = require('semver');

const version1 = '1.2.3';
const version2 = '1.3.0';

if (semver.gt(version2, version1)) {
  console.log(`${version2} is greater than ${version1}`);
} else if (semver.lt(version2, version1)) {
  console.log(`${version2} is less than ${version1}`);
} else {
  console.log(`${version2} is equal to ${version1}`);
}
```

The `semver` library provides functions like `gt` (greater than), `lt` (less than), `eq` (equal), `gte` (greater than or equal), and `lte` (less than or equal) to compare versions correctly according to the semver specification.

If you prefer a simpler approach without dependencies and your version strings are strictly in the "major.minor.patch" format, you can write a custom comparator like this:

```js
function compareVersions(v1, v2) {
  const a1 = v1.split('.').map(Number);
  const a2 = v2.split('.').map(Number);
  
  for (let i = 0; i < 3; i++) {
    if (a1[i] > a2[i]) return 1;
    if (a1[i] < a2[i]) return -1;
  }
  return 0;
}

const result = compareVersions('1.2.3', '1.3.0');
if (result > 0) {
  console.log('1.2.3 is greater');
} else if (result < 0) {
  console.log('1.3.0 is greater');
} else {
  console.log('Versions are equal');
}
```

However, this custom method won't handle pre-releases or build metadata properly, so the recommended approach is to use the `semver` library for anything beyond basic comparisons.

---



</details>

**Use a semver library for accurate version checks**  

String comparisons can misorder multi-digit versions (e.g. “1.10.0” vs “1.2.0”). I recommend using the popular `semver` package to handle all semver rules:

• Install the package:  
  ```bash
  npm install semver
  ```  
• In `apps/web/src/hooks/use-update-check.ts` (around lines 29–30):  
  ```diff
  import semver from 'semver'
  
- const latestVersion = data?.tag_name?.replace("v", "") || APP_VERSION
- const hasUpdate = latestVersion !== APP_VERSION
+ const latestVersion = data?.tag_name?.replace("v", "") || APP_VERSION
+ const hasUpdate = semver.gt(latestVersion, APP_VERSION)
  ```  

This ensures correct comparisons (including pre-releases and build metadata) using `semver.gt`, `lt`, `eq`, etc.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-10T10:13:00Z
author_association: NONE

PR comments:
user: login: vercel[bot]
created_at: 2025-05-10T10:00:44Z
body: [vc]: #lgrngsWTy+n25ehqCPFgsmwQ21OxXBLa5eHlEKxllcM=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJsZXR0ZXItc3BhY2UtZG9jcyIsInJvb3REaXJlY3RvcnkiOiJhcHBzL2RvY3MiLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoiIn0sImluc3BlY3RvclVybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS9kY29kZXNkZXZ2L2xldHRlci1zcGFjZS1kb2NzLzdOZUN3Q05RQXRya0tTR21DbXR3Rk1aMjZHZTIiLCJuZXh0Q29tbWl0U3RhdHVzIjoiSUdOT1JFRCIsInByZXZpZXdVcmwiOiJsZXR0ZXItc3BhY2UtZG9jcy1naXQtZGV2LWRjb2Rlc2RldnYudmVyY2VsLmFwcCJ9LHsibmFtZSI6ImxldHRlci1zcGFjZS1sYW5kaW5nLXBhZ2UiLCJyb290RGlyZWN0b3J5IjoiYXBwcy9sYW5kaW5nLXBhZ2UiLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoiIn0sImluc3BlY3RvclVybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS9kY29kZXNkZXZ2L2xldHRlci1zcGFjZS1sYW5kaW5nLXBhZ2UvSFZXZkhlNUxQVTduV2tLaVhmd3ZXV01xYURLaCIsIm5leHRDb21taXRTdGF0dXMiOiJJR05PUkVEIiwicHJldmlld1VybCI6ImxldHRlci1zcGFjZS1sYW5kaW5nLXBhZ2UtZ2l0LWRldi1kY29kZXNkZXZ2LnZlcmNlbC5hcHAifV19
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)


<details><summary>2 Skipped Deployments</summary>

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **letter-space-docs** | ⬜️ Ignored ([Inspect](https://vercel.com/dcodesdevv/letter-space-docs/7NeCwCNQAtrkKSGmCmtwFMZ26Ge2)) | [Visit Preview](https://letter-space-docs-git-dev-dcodesdevv.vercel.app) |  | May 10, 2025 10:18am |
| **letter-space-landing-page** | ⬜️ Ignored ([Inspect](https://vercel.com/dcodesdevv/letter-space-landing-page/HVWfHe5LPU7nWkKiXfwvWWMqaDKh)) | [Visit Preview](https://letter-space-landing-page-git-dev-dcodesdevv.vercel.app) |  | May 10, 2025 10:18am |
</details>



author_association: NONE
user: login: coderabbitai[bot]
created_at: 2025-05-10T10:00:46Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough

This update modifies the cleanup interval default from 7 to 90 days across the database schema, backend, and frontend. It introduces message status-based filtering in maintenance and statistics queries, adds message query utilities, improves division-by-zero safety in dashboard calculations, and updates documentation and deployment configurations for consistency. Additionally, it adds a React hook for update checking and integrates update notifications into the dashboard layout.

## Changes

| File(s)                                                                                     | Change Summary |
|--------------------------------------------------------------------------------------------|---------------|
| `apps/backend/prisma/migrations/.../migration.sql`, `apps/backend/prisma/schema.prisma`    | Changed the default value of `cleanupInterval` in `GeneralSettings` from 7 to 90. |
| `apps/backend/src/cron/dailyMaintenance.ts`                                                | Modified message deletion to filter by specific statuses ("SENT", "OPENED", "CLICKED", "FAILED") in addition to date. |
| `apps/backend/src/message/utils.ts`                                                        | Added new utility functions for querying messages by status, campaign, and organization using Prisma ORM. |
| `apps/backend/src/stats/query.ts`                                                          | Added status filtering and switched date field from `createdAt` to `sentAt` in statistics queries; refined campaign and message count logic. |
| `apps/backend/src/dashboard/query.ts`, `apps/web/src/pages/dashboard/campaigns/[id]/stats.tsx` | Added guard to prevent division by zero in delivery rate and sent message percentage calculations. |
| `apps/web/src/pages/dashboard/settings/general-settings.tsx`                               | Updated default, placeholder, and description for cleanup interval to 90 days in settings UI and logic. |
| `apps/web/src/hooks/use-update-check.ts`, `apps/web/src/hooks/index.ts`                    | Added new React hook `useUpdateCheck` for GitHub release version checking and exported it. |
| `apps/web/src/pages/dashboard/layout.tsx`                                                 | Integrated update notification UI in dashboard sidebar footer using `useUpdateCheck` hook. |
| `apps/docs/src/app/getting-started/page.mdx`                                               | Changed backend Docker image tag in documentation from `0` to `latest`. |
| `apps/docs/vercel.json`, `apps/landing-page/vercel.json`                                   | Enhanced `ignoreCommand` to also check for changes in `../../package.json` for deployment optimization. |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant CronJob as Daily Maintenance Cron
    participant DB as Database

    CronJob->>DB: Delete messages where status in [SENT, OPENED, CLICKED, FAILED] and createdAt < cutoff
    Note right of DB: Only messages with specified statuses and old enough are deleted
```

```mermaid
sequenceDiagram
    participant StatsProc as Stats Procedure
    participant DB as Database

    StatsProc->>DB: Query messages with status in [SENT, CLICKED, OPENED, FAILED] and sentAt in time range
    DB-->>StatsProc: Return filtered message counts
    StatsProc->>StatsProc: Calculate rates, guard against division by zero
```

```mermaid
sequenceDiagram
    participant Utils as Message Utils
    participant DB as Database

    Utils->>DB: Query messages by status, campaignId, orgId
    DB-->>Utils: Return filtered messages
    Utils-->>Caller: Return result via specific wrapper function
```

```mermaid
sequenceDiagram
    participant Dashboard as DashboardLayout Component
    participant Hook as useUpdateCheck Hook
    participant GitHub as GitHub API

    Dashboard->>Hook: Call useUpdateCheck()
    Hook->>GitHub: Fetch latest release info
    GitHub-->>Hook: Return release data
    Hook-->>Dashboard: Return hasUpdate, latestVersion
    Dashboard->>Dashboard: Conditionally render update notification link
```

## Poem

> Oh, ninety days, a longer sweep,  
> For cleanup chores that used to leap!  
> Stats are sharper, queries neat,  
> No zero-division to defeat.  
> Message filters, docs anew,  
> Deployments check what’s changed too.  
> 🐇 Cheers to code both clean and bright—  
> The garden’s growing just right!

<!-- walkthrough_end -->

<!-- announcements_start -->

> [!TIP]
> <details>
> <summary>⚡️ Faster reviews with caching</summary>
> 
> - CodeRabbit now supports caching for code and dependencies, helping speed up reviews. This means quicker feedback, reduced wait times, and a smoother review experience overall. Cached data is encrypted and stored securely. This feature will be automatically enabled for all accounts on May 16th. To opt out, configure `Review - Disable Cache` at either the organization or repository level. If you prefer to disable all data retention across your organization, simply turn off the `Data Retention` setting under your Organization Settings.
> 
> Enjoy the performance boost—your workflow just got faster.
> 
> </details>

<!-- announcements_end -->

---

<details>
<summary>📜 Recent review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 18828a5700bf800d366752931370af3fc7ae99c7 and b019c49313a31315f02fbd3daebd6a5fcae5324d.

</details>

<details>
<summary>⛔ Files ignored due to path filters (1)</summary>

* `pnpm-lock.yaml` is excluded by `!**/pnpm-lock.yaml`

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `apps/web/package.json` (2 hunks)
* `apps/web/src/hooks/use-update-check.ts` (1 hunks)

</details>

<details>
<summary>✅ Files skipped from review due to trivial changes (1)</summary>

* apps/web/package.json

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* apps/web/src/hooks/use-update-check.ts

</details>

</details>
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

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.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=dcodesdev/LetterSpace&utm_content=15):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: NONE