feat: implement unreachable calendar notifications system#23649
feat: implement unreachable calendar notifications system#23649Vansh5632 wants to merge 6 commits intocalcom:mainfrom
Conversation
|
@Vansh5632 is attempting to deploy a commit to the cal Team on Vercel. A member of the Team first needs to authorize it. |
|
Caution Review failedAn error occurred during the review process. Please try again later. ✨ Finishing Touches🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 9
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
test-calendar-notifications.js (1)
145-164: Don’t claim success unconditionally; set a failing exit code when checks failGate the success banner on prior results and set process.exitCode for CI.
-console.log("\n🎯 Testing Summary"); -console.log("=================="); -console.log("✅ All major components of the unreachable calendar notification system have been implemented!"); -console.log(""); -console.log("🔧 What was implemented:"); -console.log(" • Database schema with tracking fields"); -console.log(" • Professional email templates"); -console.log(" • Business logic with rate limiting"); -console.log(" • Google Calendar error detection"); -console.log(" • User preferences UI"); -console.log(" • tRPC API integration"); -console.log(" • Navigation and translations"); -console.log(""); -console.log("🧪 Next steps to test:"); -console.log(" 1. Visit http://localhost:3000/settings/my-account/calendar-notifications"); -console.log(" 2. Toggle the notification preference"); -console.log(" 3. Create a test scenario to trigger calendar errors"); -console.log(""); -console.log("✨ The feature is ready for production use!"); +console.log("\n🎯 Testing Summary"); +console.log("=================="); +if (__failures === 0) { + console.log("✅ All major components of the unreachable calendar notification system appear in place."); + console.log(""); + console.log("🔧 What was implemented:"); + console.log(" • Database schema with tracking fields"); + console.log(" • Professional email templates"); + console.log(" • Business logic with rate limiting"); + console.log(" • Google Calendar error detection"); + console.log(" • User preferences UI"); + console.log(" • tRPC API integration"); + console.log(" • Navigation and translations"); + console.log(""); + console.log("🧪 Next steps to test:"); + console.log(" 1. Visit http://localhost:3000/settings/my-account/calendar-notifications"); + console.log(" 2. Toggle the notification preference"); + console.log(" 3. Create a test scenario to trigger calendar errors"); + console.log(""); + console.log("✨ Ready for manual verification."); +} else { + console.log(`❌ ${__failures} check(s) failed. See logs above.`); + process.exitCode = 1; +}
🧹 Nitpick comments (20)
apps/web/public/static/locales/en/common.json (1)
529-532: Tighten copy; broaden causes beyond auth-onlyCurrent phrasing narrows the cause to “authentication issues,” but calendars can be unreachable for other reasons (revoked permission, expired token, provider outage). Suggested wording:
- "calendar_notifications_description": "Configure notifications for calendar connection issues.", - "unreachable_calendar_alerts": "Unreachable Calendar Alerts", - "unreachable_calendar_alerts_description": "Get notified when your connected calendars become inaccessible due to authentication issues.", + "calendar_notifications_description": "Configure alerts for calendar connection issues.", + "unreachable_calendar_alerts": "Unreachable calendar alerts", + "unreachable_calendar_alerts_description": "Get notified when a connected calendar becomes unreachable (e.g., revoked permissions, expired token, provider outage)."packages/prisma/migrations/20250906161941_add_unreachable_calendar_notifications/migration.sql (1)
2-9: Make booleans NOT NULL and add an index to support cooldown checksAvoid tri-state booleans and support typical queries like “unreachable and lastNotified < cutoff”.
ALTER TABLE "Credential" ADD COLUMN "isUnreachable" BOOLEAN DEFAULT false, ADD COLUMN "lastNotified" TIMESTAMP(3); +-- Enforce non-null on the boolean flag +ALTER TABLE "Credential" ALTER COLUMN "isUnreachable" SET NOT NULL; -- AlterTable ALTER TABLE "users" ADD COLUMN "notifyCalendarAlerts" BOOLEAN DEFAULT true; +ALTER TABLE "users" ALTER COLUMN "notifyCalendarAlerts" SET NOT NULL; -- CreateIndex CREATE INDEX "Credential_isUnreachable_idx" ON "Credential"("isUnreachable"); +-- Optional: composite/partial index to accelerate “cooldown” scans +CREATE INDEX "Credential_unreachable_lastNotified_idx" + ON "Credential"("lastNotified") + WHERE "isUnreachable" = true;packages/app-store/_utils/invalidateCredential.ts (1)
22-24: Make notification marking best-effort (don’t block invalidation)
WrapmarkCredentialAsUnreachablein a best-effort call so transient failures don’t interrupt the invalidation flow.- // Also mark as unreachable and send notification if appropriate - await markCredentialAsUnreachable(credentialId, "Credential invalidated due to authentication failure"); + // Also mark as unreachable and send notification if appropriate (best-effort) + await markCredentialAsUnreachable( + credentialId, + "Credential invalidated due to authentication failure" + ).catch((err) => console.warn("markCredentialAsUnreachable failed", { credentialId, err }));packages/prisma/schema.prisma (1)
272-275: Credential reachability fields: consider query patterns
Index onisUnreachableis good. If you frequently fetch unreachable creds per-user (e.g., digests/cleanup), add a composite index.model Credential { // ... isUnreachable Boolean? @default(false) lastNotified DateTime? // ... @@index([appId]) @@index([subscriptionId]) @@index([invalid]) @@index([userId, delegationCredentialId]) @@index([isUnreachable]) + @@index([userId, isUnreachable], name: "Credential_userId_isUnreachable_idx") }Also applies to: 280-281
packages/app-store/googlecalendar/lib/CalendarService.ts (2)
457-468: Only flip reachability on successful responses and avoid write amplification
Gate the “reachable” write behind a 2xx/3xx check, and consider updating only whenisUnreachableis currently true (adjust helper toupdateManywith a WHERE).- const apiResponse = await this.auth.authManager.request( + const { json, status } = await this.auth.authManager.request( async () => new AxiosLikeResponseToFetchResponse(await calendar.freebusy.query({ requestBody })) ); - - // Mark credential as reachable on successful API call - await markCredentialAsReachable(this.credential.id); - - return apiResponse.json; + // Mark credential as reachable only on success + if (status >= 200 && status < 400) { + await markCredentialAsReachable(this.credential.id); + } + return json;Additionally, in
markCredentialAsReachable(outside this file), switch to a conditional update:// packages/lib/markCredentialAsUnreachable.ts await prisma.credential.updateMany({ where: { id: credentialId, isUnreachable: true }, data: { isUnreachable: false }, });
1154-1190: Simplify and de-duplicate auth error detection
Remove duplicatedstatuschecks and handle common shapes (response.status, numeric/stringcode).- private isAuthenticationError(error: unknown): boolean { - // Check for HTTP status codes - if (error && typeof error === "object") { - // Handle GaxiosResponse errors - if ("status" in error && typeof error.status === "number") { - return error.status === 401 || error.status === 403; - } - - // Handle Response objects - if ("status" in error && typeof error.status === "number") { - return error.status === 401 || error.status === 403; - } - - // Handle Google API specific errors - if ("message" in error && typeof error.message === "string") { - const message = error.message.toLowerCase(); - return ( - message.includes("invalid_grant") || - message.includes("unauthorized") || - message.includes("forbidden") || - message.includes("invalid credentials") || - message.includes("token expired") || - message.includes("access denied") - ); - } - - // Handle errors with code property - if ("code" in error && typeof error.code === "number") { - return error.code === 401 || error.code === 403; - } - } - return false; - } + private isAuthenticationError(error: unknown): boolean { + if (!error || typeof error !== "object") return false; + const e: any = error; + const status = + typeof e.status === "number" + ? e.status + : typeof e.response?.status === "number" + ? e.response.status + : undefined; + if (status === 401 || status === 403) return true; + const codeNum = + typeof e.code === "number" ? e.code : Number.isFinite(Number(e.code)) ? Number(e.code) : undefined; + if (codeNum === 401 || codeNum === 403) return true; + const msg = String(e.message || "").toLowerCase(); + return ["invalid_grant", "unauthorized", "forbidden", "invalid credentials", "token expired", "access denied"].some( + (s) => msg.includes(s) + ); + }apps/web/modules/settings/my-account/calendar-notifications-view.tsx (2)
11-12: Prefer named export over default for consistency and tree-shaking.This isn’t a Next.js page component; consider a named export.
-const CalendarNotificationsView = () => { +export const CalendarNotificationsView = () => { ... -export default CalendarNotificationsView; +// If other modules import default, keep this temporarily or update imports: +// export default CalendarNotificationsView;Also applies to: 59-59
16-26: Guard against double submits; use mutation status and finally block.Minor UX hardening: rely on mutation status rather than manual
setLoadingin multiple places.- const updateProfileMutation = trpc.viewer.me.updateProfile.useMutation({ - onSuccess: () => { - showToast(t("settings_updated_successfully"), "success"); - refetch(); - setLoading(false); - }, - onError: (error) => { - showToast(error.message, "error"); - setLoading(false); - }, - }); + const updateProfileMutation = trpc.viewer.me.updateProfile.useMutation({ + onSuccess: () => { + showToast(t("settings_updated_successfully"), "success"); + refetch(); + }, + onError: (error) => { + showToast(error.message, "error"); + }, + onSettled: () => setLoading(false), + });Also applies to: 28-33
packages/lib/markCredentialAsUnreachable.ts (1)
66-74: Prefer centralized logger overconsole.*.Use the project logger for consistency and structured logs.
- console.log( - `Sent unreachable calendar notification to ${credential.user.email} for credential ${credentialId}` - ); + log.info("unreachable_calendar_email_sent", { + credentialId, + userId: credential.user.id, + email: credential.user.email, + }); ... - console.log(`Marked credential ${credentialId} as unreachable`, error ? `Error: ${error}` : ""); + log.warn("credential_marked_unreachable", { credentialId, error }); ... - console.error(`Failed to mark credential ${credentialId} as unreachable:`, err); + log.error("credential_mark_unreachable_failed", { credentialId, err });Note:
logimport depends on your logger util (e.g.,import { log } from "@calcom/lib/logger";).packages/emails/templates/unreachable-calendar-email.ts (4)
1-3: Consolidate imports from the same module.-import { EMAIL_FROM_NAME } from "@calcom/lib/constants"; -import { WEBAPP_URL } from "@calcom/lib/constants"; +import { EMAIL_FROM_NAME, WEBAPP_URL } from "@calcom/lib/constants";
41-69: Escape dynamic values in HTML to avoid broken markup and reduce injection risk.Minimal escaping for
calendarName,recipientName, andreason.+ private esc(s?: string) { + return (s ?? "") + .replace(/&/g, "&") + .replace(/</g, "<") + .replace(/>/g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); + } ... - <h2>Action Required: Your ${this.calendarName} is unreachable</h2> + <h2>Action Required: Your ${this.esc(this.calendarName)} is unreachable</h2> ... - ${this.recipientName ? `Hi ${this.recipientName},` : "Hello,"} + ${this.recipientName ? `Hi ${this.esc(this.recipientName)},` : "Hello,"} ... - Your linked calendar <strong>${this.calendarName}</strong> is no longer reachable. - ${this.reason ? `Reason: ${this.reason}.` : ""} + Your linked calendar <strong>${this.esc(this.calendarName)}</strong> is no longer reachable. + ${this.reason ? `Reason: ${this.esc(this.reason)}.` : ""}Also applies to: 85-107
35-45: Unify subject and heading phrasing.Use the same phrasing (“Action needed” vs “Action Required”) for consistency.
- subject: `Action needed: Your ${this.calendarName} is unreachable`, + subject: `Action required: Your ${this.calendarName} is unreachable`, ... - return `Action Required: Your ${this.calendarName} is unreachable + return `Action required: Your ${this.calendarName} is unreachableAlso applies to: 86-90
6-6: Prefer named export over default per repo convention.-export default class UnreachableCalendarEmail extends BaseEmail { +export class UnreachableCalendarEmail extends BaseEmail {Note: Ensure
email-manager.tsimports updated symbol.test-calendar-notifications.js (7)
3-8: Fix misleading header commentThis script performs static repository checks; it does not simulate runtime notification flows. Update the comment to avoid confusion.
/** * Test script for Calendar Notifications Feature * - * This script tests the unreachable calendar notification system we just implemented. - * It simulates triggering the notification flow. + * Static smoke checks for the unreachable calendar notification system. + * Verifies presence of files, symbols, and wiring; it does not execute runtime flows. */
35-62: Strengthen Google Calendar checks to verify import and usage, not just substringsSimple substring matches can pass even if symbols aren’t imported/used. Add import/usage checks to reduce false positives.
if (content.includes("isAuthenticationError")) { console.log("✅ Authentication error detection added"); } else { console.log("❌ Authentication error detection missing"); } if (content.includes("markCredentialAsUnreachable")) { console.log("✅ Unreachable marking integration added"); } else { console.log("❌ Unreachable marking integration missing"); } if (content.includes("markCredentialAsReachable")) { console.log("✅ Reachable marking integration added"); } else { console.log("❌ Reachable marking integration missing"); } + + // Extra: verify import + usage presence + if (/import[\s\S]*\bmarkCredentialAsUnreachable\b/.test(content)) { + console.log("✅ markCredentialAsUnreachable import present"); + } else { + console.log("❌ markCredentialAsUnreachable import missing"); + } + if (/\bmarkCredentialAsUnreachable\s*\(/.test(content)) { + console.log("✅ markCredentialAsUnreachable is invoked"); + } else { + console.log("❌ markCredentialAsUnreachable not invoked"); + } + if (/import[\s\S]*\bmarkCredentialAsReachable\b/.test(content)) { + console.log("✅ markCredentialAsReachable import present"); + } else { + console.log("❌ markCredentialAsReachable import missing"); + } + if (/\bmarkCredentialAsReachable\s*\(/.test(content)) { + console.log("✅ markCredentialAsReachable is invoked"); + } else { + console.log("❌ markCredentialAsReachable not invoked"); + } } else { console.log("❌ Google Calendar Service file not found"); }
67-87: Scope Prisma checks to actual models to avoid false positivesRaw “includes” can match comments/unrelated blocks. Anchor to Credential/User model blocks.
if (fs.existsSync(schemaPath)) { const content = fs.readFileSync(schemaPath, "utf8"); - if (content.includes("isUnreachable")) { - console.log("✅ Credential.isUnreachable field added"); - } else { - console.log("❌ Credential.isUnreachable field missing"); - } - - if (content.includes("lastNotified")) { - console.log("✅ Credential.lastNotified field added"); - } else { - console.log("❌ Credential.lastNotified field missing"); - } - - if (content.includes("notifyCalendarAlerts")) { - console.log("✅ User.notifyCalendarAlerts field added"); - } else { - console.log("❌ User.notifyCalendarAlerts field missing"); - } + const credModel = content.match(/model\s+Credential\s*{[^}]*}/s); + const userModel = content.match(/model\s+User\s*{[^}]*}/s); + if (credModel && /\bisUnreachable\b/.test(credModel[0])) { + console.log("✅ Credential.isUnreachable field added"); + } else { + console.log("❌ Credential.isUnreachable field missing"); + } + if (credModel && /\blastNotified\b/.test(credModel[0])) { + console.log("✅ Credential.lastNotified field added"); + } else { + console.log("❌ Credential.lastNotified field missing"); + } + if (userModel && /\bnotifyCalendarAlerts\b/.test(userModel[0])) { + console.log("✅ User.notifyCalendarAlerts field added"); + } else { + console.log("❌ User.notifyCalendarAlerts field missing"); + } } else { console.log("❌ Prisma schema file not found"); }
114-122: Tighten navigation checkAlso look for the concrete href to reduce false positives.
- if (content.includes("calendar-notifications")) { + if ( + content.includes("calendar-notifications") || + content.includes('"/settings/my-account/calendar-notifications"') || + content.includes("/settings/my-account/calendar-notifications") + ) { console.log("✅ Calendar notifications menu item added"); } else { console.log("❌ Calendar notifications menu item missing"); }
16-33: Optional: resolve repo root robustlyIf this script ever moves out of repo root, __dirname-based paths will break. Resolve the repository root via Git when available.
// near the top const { execSync } = require("child_process"); let REPO_ROOT = __dirname; try { REPO_ROOT = execSync("git rev-parse --show-toplevel", { stdio: ["ignore", "pipe", "ignore"] }) .toString() .trim(); } catch {} // then replace path.join(__dirname, ...) with path.join(REPO_ROOT, ...)
1-164: Consider moving this into the test suite and CIWrap these checks in Jest/Vitest with real assertions so they run on every PR and fail fast.
133-141: Optional: strengthen schema check in test
The updateProfile schema (packages/trpc/server/routers/viewer/me/updateProfile.schema.ts) already definesnotifyCalendarAlerts: z.boolean().optional()so the current
content.includes("notifyCalendarAlerts")check will pass. To avoid false positives if that string appears elsewhere, you can tighten the regex to ensure it’s part of a Zod object, for example:- if (content.includes("notifyCalendarAlerts")) { + if (/\bnotifyCalendarAlerts\b/.test(content) && /\bz\.object\b/.test(content)) { console.log("✅ tRPC updateProfile schema updated"); } else { console.log("❌ tRPC updateProfile schema missing notifyCalendarAlerts"); }
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (14)
apps/web/app/(use-page-wrapper)/settings/(settings-layout)/SettingsLayoutAppDirClient.tsx(1 hunks)apps/web/app/(use-page-wrapper)/settings/(settings-layout)/my-account/calendar-notifications/page.tsx(1 hunks)apps/web/modules/settings/my-account/calendar-notifications-view.tsx(1 hunks)apps/web/public/static/locales/en/common.json(1 hunks)packages/app-store/_utils/invalidateCredential.ts(2 hunks)packages/app-store/googlecalendar/lib/CalendarService.ts(4 hunks)packages/emails/email-manager.ts(2 hunks)packages/emails/templates/unreachable-calendar-email.ts(1 hunks)packages/lib/markCredentialAsUnreachable.ts(1 hunks)packages/prisma/migrations/20250906161941_add_unreachable_calendar_notifications/migration.sql(1 hunks)packages/prisma/schema.prisma(2 hunks)packages/trpc/server/routers/viewer/me/get.handler.ts(1 hunks)packages/trpc/server/routers/viewer/me/updateProfile.schema.ts(1 hunks)test-calendar-notifications.js(1 hunks)
🧰 Additional context used
📓 Path-based instructions (5)
**/*.ts
📄 CodeRabbit inference engine (.cursor/rules/review.mdc)
**/*.ts: For Prisma queries, only select data you need; never useinclude, always useselect
Ensure thecredential.keyfield is never returned from tRPC endpoints or APIs
Files:
packages/trpc/server/routers/viewer/me/get.handler.tspackages/app-store/_utils/invalidateCredential.tspackages/lib/markCredentialAsUnreachable.tspackages/emails/email-manager.tspackages/trpc/server/routers/viewer/me/updateProfile.schema.tspackages/app-store/googlecalendar/lib/CalendarService.tspackages/emails/templates/unreachable-calendar-email.ts
**/*.{ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/review.mdc)
Flag excessive Day.js use in performance-critical code; prefer native Date or Day.js
.utc()in hot paths like loops
Files:
packages/trpc/server/routers/viewer/me/get.handler.tsapps/web/app/(use-page-wrapper)/settings/(settings-layout)/SettingsLayoutAppDirClient.tsxpackages/app-store/_utils/invalidateCredential.tspackages/lib/markCredentialAsUnreachable.tspackages/emails/email-manager.tsapps/web/modules/settings/my-account/calendar-notifications-view.tsxpackages/trpc/server/routers/viewer/me/updateProfile.schema.tsapps/web/app/(use-page-wrapper)/settings/(settings-layout)/my-account/calendar-notifications/page.tsxpackages/app-store/googlecalendar/lib/CalendarService.tspackages/emails/templates/unreachable-calendar-email.ts
**/*.{ts,tsx,js,jsx}
⚙️ CodeRabbit configuration file
Flag default exports and encourage named exports. Named exports provide better tree-shaking, easier refactoring, and clearer imports. Exempt main components like pages, layouts, and components that serve as the primary export of a module.
Files:
packages/trpc/server/routers/viewer/me/get.handler.tsapps/web/app/(use-page-wrapper)/settings/(settings-layout)/SettingsLayoutAppDirClient.tsxpackages/app-store/_utils/invalidateCredential.tstest-calendar-notifications.jspackages/lib/markCredentialAsUnreachable.tspackages/emails/email-manager.tsapps/web/modules/settings/my-account/calendar-notifications-view.tsxpackages/trpc/server/routers/viewer/me/updateProfile.schema.tsapps/web/app/(use-page-wrapper)/settings/(settings-layout)/my-account/calendar-notifications/page.tsxpackages/app-store/googlecalendar/lib/CalendarService.tspackages/emails/templates/unreachable-calendar-email.ts
**/*.tsx
📄 CodeRabbit inference engine (.cursor/rules/review.mdc)
Always use
t()for text localization in frontend code; direct text embedding should trigger a warning
Files:
apps/web/app/(use-page-wrapper)/settings/(settings-layout)/SettingsLayoutAppDirClient.tsxapps/web/modules/settings/my-account/calendar-notifications-view.tsxapps/web/app/(use-page-wrapper)/settings/(settings-layout)/my-account/calendar-notifications/page.tsx
**/*Service.ts
📄 CodeRabbit inference engine (.cursor/rules/review.mdc)
Service files must include
Servicesuffix, use PascalCase matching exported class, and avoid generic names (e.g.,MembershipService.ts)
Files:
packages/app-store/googlecalendar/lib/CalendarService.ts
🧠 Learnings (4)
📓 Common learnings
Learnt from: ShashwatPS
PR: calcom/cal.com#23638
File: packages/trpc/server/routers/viewer/calendars/setDestinationReminder.handler.test.ts:198-199
Timestamp: 2025-09-06T11:00:34.348Z
Learning: In calcom/cal.com PR #23638, the maintainer ShashwatPS determined that authorization checks in the setDestinationReminder handler are "not applicable" when CodeRabbit suggested adding user-scoped WHERE clauses to prevent users from modifying other users' destination calendar reminder settings.
Learnt from: anglerfishlyy
PR: calcom/cal.com#0
File: :0-0
Timestamp: 2025-08-27T16:39:38.192Z
Learning: anglerfishlyy successfully implemented CAL-3076 email invitation feature for Cal.com team event-types in PR #23312. The feature allows inviting people via email directly from assignment flow, with automatic team invitation if email doesn't belong to existing team member. Implementation includes Host type modifications (userId?: number, email?: string, isPending?: boolean), CheckedTeamSelect component updates with CreatableSelect, TRPC schema validation with zod email validation, and integration with existing teamInvite system.
📚 Learning: 2025-09-01T08:56:14.071Z
Learnt from: nangelina
PR: calcom/cal.com#23486
File: packages/app-store/kyzon-space/lib/tokenManager.ts:25-31
Timestamp: 2025-09-01T08:56:14.071Z
Learning: In token refresh utilities like tokenManager.ts, accessing credential.key from Prisma is legitimate and necessary for OAuth token refresh flows. These internal utilities need stored credentials to refresh tokens and don't expose them in API responses.
Applied to files:
packages/app-store/_utils/invalidateCredential.ts
📚 Learning: 2025-08-05T12:04:29.037Z
Learnt from: din-prajapati
PR: calcom/cal.com#21854
File: packages/app-store/office365calendar/__tests__/unit_tests/SubscriptionManager.test.ts:0-0
Timestamp: 2025-08-05T12:04:29.037Z
Learning: In packages/app-store/office365calendar/lib/CalendarService.ts, the fetcher method in Office365CalendarService class is public, not private. It was specifically changed from private to public in this PR to support proper testing and external access patterns.
Applied to files:
packages/app-store/googlecalendar/lib/CalendarService.ts
📚 Learning: 2025-07-18T17:57:16.395Z
Learnt from: vijayraghav-io
PR: calcom/cal.com#21072
File: packages/app-store/office365calendar/api/webhook.ts:120-123
Timestamp: 2025-07-18T17:57:16.395Z
Learning: The office365calendar webhook handler in packages/app-store/office365calendar/api/webhook.ts is specifically designed for Office365 calendar integration, not as a generic webhook handler. Therefore, it's safe to assume that fetchAvailabilityAndSetCache method will be implemented in the Office365CalendarService, making explicit validation checks unnecessary.
Applied to files:
packages/app-store/googlecalendar/lib/CalendarService.ts
🧬 Code graph analysis (7)
packages/app-store/_utils/invalidateCredential.ts (1)
packages/lib/markCredentialAsUnreachable.ts (1)
markCredentialAsUnreachable(12-75)
packages/lib/markCredentialAsUnreachable.ts (2)
packages/lib/server/repository/credential.ts (1)
CredentialRepository(29-289)packages/emails/email-manager.ts (1)
sendUnreachableCalendarEmail(892-912)
packages/emails/email-manager.ts (2)
packages/emails/templates/_base-email.ts (1)
sendEmail(32-97)packages/emails/templates/unreachable-calendar-email.ts (1)
UnreachableCalendarEmail(6-109)
apps/web/modules/settings/my-account/calendar-notifications-view.tsx (1)
apps/web/app/_trpc/trpc.ts (1)
trpc(7-7)
apps/web/app/(use-page-wrapper)/settings/(settings-layout)/my-account/calendar-notifications/page.tsx (1)
apps/web/app/_utils.tsx (1)
_generateMetadata(53-81)
packages/app-store/googlecalendar/lib/CalendarService.ts (1)
packages/lib/markCredentialAsUnreachable.ts (1)
markCredentialAsReachable(81-94)
packages/emails/templates/unreachable-calendar-email.ts (2)
packages/emails/templates/_base-email.ts (1)
BaseEmail(14-110)packages/lib/constants.ts (2)
EMAIL_FROM_NAME(32-32)WEBAPP_URL(12-18)
🔇 Additional comments (8)
apps/web/app/(use-page-wrapper)/settings/(settings-layout)/SettingsLayoutAppDirClient.tsx (1)
42-42: Approve calendar notifications nav entry — page.tsx exports generateMetadata and default Page; “calendar_notifications” key exists.packages/emails/email-manager.ts (1)
82-82: No action required: opt-in and cooldown checks are already implemented
The call site in packages/lib/markCredentialAsUnreachable.ts uses shouldSendNotification(credential.lastNotified, credential.user.notifyCalendarAlerts) andif (shouldNotify && credential.user.email)before calling sendUnreachableCalendarEmail, and updates lastNotified only on send.packages/app-store/_utils/invalidateCredential.ts (1)
1-1: Use Prismaselectinstead ofincludein markCredentialAsUnreachable
The current implementation (packages/lib/markCredentialAsUnreachable.ts) usesincludeonprisma.credential.findUnique. Per repo guidelines for**/*.ts, preferselecteverywhere.Replace the query with
select(and keepkeyout of any API surface):// In packages/lib/markCredentialAsUnreachable.ts const credential = await prisma.credential.findUnique({ where: { id: credentialId }, select: { id: true, type: true, lastNotified: true, user: { select: { id: true, email: true, name: true, notifyCalendarAlerts: true } }, app: { select: { name: true, slug: true } }, }, });⛔ Skipped due to learnings
Learnt from: CR PR: calcom/cal.com#0 File: .cursor/rules/review.mdc:0-0 Timestamp: 2025-07-28T11:50:23.946Z Learning: Applies to **/*.ts : For Prisma queries, only select data you need; never use `include`, always use `select`Learnt from: Udit-takkar PR: calcom/cal.com#22919 File: packages/lib/server/repository/PrismaPhoneNumberRepository.ts:412-417 Timestamp: 2025-08-07T18:42:34.081Z Learning: In Cal.com codebase, the coding guideline requiring explicit `select` clauses instead of `include` for Prisma queries applies to read operations but not to update operations. Update operations don't need explicit select clauses.Learnt from: CR PR: calcom/cal.com#0 File: .cursor/rules/review.mdc:0-0 Timestamp: 2025-07-28T11:50:23.946Z Learning: Applies to **/*.ts : Ensure the `credential.key` field is never returned from tRPC endpoints or APIsLearnt from: supalarry PR: calcom/cal.com#23514 File: apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts:579-582 Timestamp: 2025-09-03T11:54:05.409Z Learning: In calcom/cal.com bookings repository methods, when Prisma include uses `eventType: true`, all eventType fields including seatsShowAttendees are automatically included in the selection. Explicit field selection is not required when using `true` for nested relations.Learnt from: supalarry PR: calcom/cal.com#23514 File: apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts:579-582 Timestamp: 2025-09-03T11:54:05.409Z Learning: In calcom/cal.com bookings repository methods, when Prisma include uses `eventType: true`, all eventType fields including seatsShowAttendees are automatically included in the selection. Explicit field selection is not required when using `true` for nested relations.Learnt from: supalarry PR: calcom/cal.com#23514 File: apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts:579-582 Timestamp: 2025-09-03T11:54:05.409Z Learning: In calcom/cal.com bookings repository methods, when Prisma select uses `eventType: true`, all eventType fields including seatsShowAttendees are automatically included in the selection. Explicit field selection is not required when using `true` for nested relations.packages/trpc/server/routers/viewer/me/updateProfile.schema.ts (1)
24-24: LGTM OptionalnotifyCalendarAlertstoggle is correctly implemented and scoped to the authenticated user.packages/app-store/googlecalendar/lib/CalendarService.ts (2)
15-15: LGTM: reachable marker import
Import location is consistent with the new flow.
783-791: Verify OAuthManager invalidation on Google auth errors
Ensure that Google’s 401/403 orinvalid_grantresponses are handled by OAuthManager and invoke the providedinvalidateCredentialcallback (thus triggeringmarkCredentialAsUnreachable).apps/web/app/(use-page-wrapper)/settings/(settings-layout)/my-account/calendar-notifications/page.tsx (1)
5-12: LGTM: metadata + routing wired to i18n keys
Page metadata usest()keys and path is correct. Default export is appropriate for a Next.js page.test-calendar-notifications.js (1)
1-2: CLI entrypoint looks goodShebang and CommonJS imports are appropriate for a repo-local Node script.
| const { data: user, refetch } = trpc.viewer.me.get.useQuery(); | ||
| const updateProfileMutation = trpc.viewer.me.updateProfile.useMutation({ |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Avoid defaulting the toggle to “on” before data loads; wire loading state to disable the control.
Defaulting checked to true when user hasn’t loaded can misrepresent user prefs and cause a flicker. Also disable the switch while the user query is loading.
- const { data: user, refetch } = trpc.viewer.me.get.useQuery();
+ const { data: user, refetch, isLoading: isUserLoading } = trpc.viewer.me.get.useQuery();
...
- <Switch
- checked={user?.notifyCalendarAlerts ?? true}
- onCheckedChange={handleToggle}
- disabled={loading}
- />
+ <Switch
+ checked={Boolean(user?.notifyCalendarAlerts)}
+ onCheckedChange={handleToggle}
+ disabled={loading || isUserLoading}
+ aria-label={t("unreachable_calendar_alerts")}
+ />Also applies to: 48-52
🤖 Prompt for AI Agents
In apps/web/modules/settings/my-account/calendar-notifications-view.tsx around
lines 15-16 and 48-52, the toggle is defaulting to "on" before the user data
loads and isn't disabled during loading; update the trpc query call to
destructure isLoading (const { data: user, refetch, isLoading } =
trpc.viewer.me.get.useQuery()), and change the Switch/Switch-like control to
derive checked from the loaded user (e.g.
checked={!!user?.preferences?.calendarNotifications} or checked={user ?
Boolean(user.preferences?.calendarNotifications) : false}) instead of a
hardcoded true, and set disabled={isLoading} so the control is disabled while
the query is in-flight; ensure the mutation still works once user is present.
| export const sendUnreachableCalendarEmail = async ({ | ||
| recipientEmail, | ||
| recipientName, | ||
| calendarName, | ||
| reason, | ||
| }: { | ||
| recipientEmail: string; | ||
| recipientName?: string; | ||
| calendarName: string; | ||
| reason: string; | ||
| }) => { | ||
| await sendEmail( | ||
| () => | ||
| new UnreachableCalendarEmail({ | ||
| recipientEmail, | ||
| recipientName, | ||
| calendarName, | ||
| reason, | ||
| }) | ||
| ); | ||
| }; |
There was a problem hiding this comment.
Sanitize dynamic values used in the email template (subject/body) to prevent HTML/header injection
calendarName, recipientName, and especially reason may contain provider/error strings. Ensure they’re HTML-escaped in the template (and header-safe in subject). If not handled in the template, add escaping there.
Below change applies to packages/emails/templates/unreachable-calendar-email.ts:
@@
export default class UnreachableCalendarEmail extends BaseEmail {
@@
- protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
+ private esc(s?: string) {
+ if (!s) return "";
+ return String(s)
+ .replace(/&/g, "&")
+ .replace(/</g, "<")
+ .replace(/>/g, ">")
+ .replace(/"/g, """)
+ .replace(/'/g, "'");
+ }
+
+ protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
return {
- from: `${EMAIL_FROM_NAME} <${this.getMailerOptions().from}>`,
- to: this.recipientEmail,
- subject: `Action needed: Your ${this.calendarName} is unreachable`,
+ from: `${EMAIL_FROM_NAME} <${this.getMailerOptions().from}>`,
+ to: this.recipientEmail,
+ subject: `Action needed: Your ${this.esc(this.calendarName)} is unreachable`,
html: await this.getHtml(),
text: this.getTextBody(),
};
}
async getHtml() {
return `
<div style="font-family: Arial, sans-serif;">
- <h2>Action Required: Your ${this.calendarName} is unreachable</h2>
+ <h2>Action Required: Your ${this.esc(this.calendarName)} is unreachable</h2>
<p>
- ${this.recipientName ? `Hi ${this.recipientName},` : "Hello,"}
+ ${this.recipientName ? `Hi ${this.esc(this.recipientName)},` : "Hello,"}
</p>
<p>
- Your linked calendar <strong>${this.calendarName}</strong> is no longer reachable.
- ${this.reason ? `Reason: ${this.reason}.` : ""}
+ Your linked calendar <strong>${this.esc(this.calendarName)}</strong> is no longer reachable.
+ ${this.reason ? `Reason: ${this.esc(this.reason)}.` : ""}
Until you reconnect or remove it, invitees will see no availability on your booking pages.
</p>
@@
protected getTextBody(): string {
- return `Action Required: Your ${this.calendarName} is unreachable
+ return `Action Required: Your ${this.calendarName} is unreachable
-${this.recipientName ? `Hi ${this.recipientName},` : "Hello,"}
+${this.recipientName ? `Hi ${this.recipientName},` : "Hello,"}
Your linked calendar ${this.calendarName} is no longer reachable. ${
this.reason ? `Reason: ${this.reason}.` : ""
} Until you reconnect or remove it, invitees will see no availability on your booking pages.Optionally, also escape values in the text body for consistency, though risk is lower there.
📝 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.
| export const sendUnreachableCalendarEmail = async ({ | |
| recipientEmail, | |
| recipientName, | |
| calendarName, | |
| reason, | |
| }: { | |
| recipientEmail: string; | |
| recipientName?: string; | |
| calendarName: string; | |
| reason: string; | |
| }) => { | |
| await sendEmail( | |
| () => | |
| new UnreachableCalendarEmail({ | |
| recipientEmail, | |
| recipientName, | |
| calendarName, | |
| reason, | |
| }) | |
| ); | |
| }; | |
| export default class UnreachableCalendarEmail extends BaseEmail { | |
| // Escape helper to prevent HTML/header injection | |
| private esc(s?: string): string { | |
| if (!s) return ""; | |
| return String(s) | |
| .replace(/&/g, "&") | |
| .replace(/</g, "<") | |
| .replace(/>/g, ">") | |
| .replace(/"/g, """) | |
| .replace(/'/g, "'"); | |
| } | |
| protected async getNodeMailerPayload(): Promise<Record<string, unknown>> { | |
| return { | |
| from: `${EMAIL_FROM_NAME} <${this.getMailerOptions().from}>`, | |
| to: this.recipientEmail, | |
| subject: `Action needed: Your ${this.esc(this.calendarName)} is unreachable`, | |
| html: await this.getHtml(), | |
| text: this.getTextBody(), | |
| }; | |
| } | |
| async getHtml(): Promise<string> { | |
| return ` | |
| <div style="font-family: Arial, sans-serif;"> | |
| <h2>Action Required: Your ${this.esc(this.calendarName)} is unreachable</h2> | |
| <p> | |
| ${this.recipientName ? `Hi ${this.esc(this.recipientName)},` : "Hello,"} | |
| </p> | |
| <p> | |
| Your linked calendar <strong>${this.esc(this.calendarName)}</strong> is no longer reachable. | |
| ${this.reason ? `Reason: ${this.esc(this.reason)}.` : ""} | |
| Until you reconnect or remove it, invitees will see no availability on your booking pages. | |
| </p> | |
| </div> | |
| `; | |
| } | |
| protected getTextBody(): string { | |
| return `Action Required: Your ${this.calendarName} is unreachable | |
| ${this.recipientName ? `Hi ${this.recipientName},` : "Hello,"} | |
| Your linked calendar ${this.calendarName} is no longer reachable. ${ | |
| this.reason ? `Reason: ${this.reason}.` : "" | |
| } Until you reconnect or remove it, invitees will see no availability on your booking pages. | |
| `; | |
| } | |
| } |
| const credential = await prisma.credential.findUnique({ | ||
| where: { id: credentialId }, | ||
| include: { | ||
| user: { | ||
| select: { | ||
| id: true, | ||
| email: true, | ||
| name: true, | ||
| notifyCalendarAlerts: true, | ||
| }, | ||
| }, | ||
| app: { | ||
| select: { | ||
| name: true, | ||
| slug: true, | ||
| }, | ||
| }, | ||
| }, | ||
| }); |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Use Prisma select (not include) and limit fields per house rules.
Aligns with repo guidelines and avoids ever fetching sensitive fields.
- const credential = await prisma.credential.findUnique({
- where: { id: credentialId },
- include: {
- user: {
- select: {
- id: true,
- email: true,
- name: true,
- notifyCalendarAlerts: true,
- },
- },
- app: {
- select: {
- name: true,
- slug: true,
- },
- },
- },
- });
+ const credential = await prisma.credential.findUnique({
+ where: { id: credentialId },
+ select: {
+ id: true,
+ type: true,
+ lastNotified: true,
+ user: {
+ select: { id: true, email: true, name: true, notifyCalendarAlerts: true },
+ },
+ app: {
+ select: { name: true, slug: true },
+ },
+ },
+ });📝 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.
| const credential = await prisma.credential.findUnique({ | |
| where: { id: credentialId }, | |
| include: { | |
| user: { | |
| select: { | |
| id: true, | |
| email: true, | |
| name: true, | |
| notifyCalendarAlerts: true, | |
| }, | |
| }, | |
| app: { | |
| select: { | |
| name: true, | |
| slug: true, | |
| }, | |
| }, | |
| }, | |
| }); | |
| const credential = await prisma.credential.findUnique({ | |
| where: { id: credentialId }, | |
| select: { | |
| id: true, | |
| type: true, | |
| lastNotified: true, | |
| user: { | |
| select: { | |
| id: true, | |
| email: true, | |
| name: true, | |
| notifyCalendarAlerts: true, | |
| }, | |
| }, | |
| app: { | |
| select: { | |
| name: true, | |
| slug: true, | |
| }, | |
| }, | |
| }, | |
| }); |
| // receive email notifications for unreachable calendars or not | ||
| notifyCalendarAlerts Boolean? @default(true) | ||
|
|
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
Backfill existing rows to avoid tri-state nulls
notifyCalendarAlerts Boolean? @default(true) will be null for existing rows unless backfilled. Ensure the migration sets existing users.notifyCalendarAlerts to true.
🏁 Script executed:
#!/bin/bash
# Inspect the migration to confirm default + backfill
fd -a 'migrations' packages/prisma | head -n 5
rg -nP -C2 'notifyCalendarAlerts' packages/prisma/migrations
rg -nP -C3 'ALTER TABLE.*users.*ADD COLUMN.*notifyCalendarAlerts' packages/prisma/migrations
rg -nP -C3 'UPDATE\s+\"?users\"?.*notifyCalendarAlerts\s*=\s*true' packages/prisma/migrationsLength of output: 2002
Add explicit backfill for notifyCalendarAlerts (packages/prisma/migrations/20250906161941_add_unreachable_calendar_notifications/migration.sql): after the ALTER TABLE ... ADD COLUMN "notifyCalendarAlerts" BOOLEAN DEFAULT true;, add
UPDATE "users"
SET "notifyCalendarAlerts" = true
WHERE "notifyCalendarAlerts" IS NULL;to ensure no existing rows remain null.
🤖 Prompt for AI Agents
In packages/prisma/schema.prisma around lines 396-398, the new optional Boolean
field notifyCalendarAlerts has a default of true but the migration file
packages/prisma/migrations/20250906161941_add_unreachable_calendar_notifications/migration.sql
must explicitly backfill existing rows; edit that migration to add an UPDATE
"users" SET "notifyCalendarAlerts" = true WHERE "notifyCalendarAlerts" IS NULL;
immediately after the ALTER TABLE ... ADD COLUMN statement so no existing rows
remain NULL, then regenerate/apply the migration so the database reflects the
backfill.
| notifyCalendarAlerts: user.notifyCalendarAlerts, | ||
| ...profileData, |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
Ensure notifyCalendarAlerts is loaded by the repository
If UserRepository.enrichUserWithTheProfile selects explicit columns, user.notifyCalendarAlerts could be undefined. Confirm it’s selected; otherwise the UI toggle may misread state.
🏁 Script executed:
#!/usr/bin/env bash
# Find repository selection of user fields
rg -nP 'enrichUserWithTheProfile|notifyCalendarAlerts' -C3 packages
# Also confirm update schema supports this field
rg -n 'notifyCalendarAlerts' packages/trpc/server/routers/viewer/me/updateProfile.schema.ts -nLength of output: 6855
🏁 Script executed:
#!/usr/bin/env bash
# Display implementation of enrichUserWithTheProfile to verify selected fields
sed -n '370,450p' packages/lib/server/repository/user.tsLength of output: 2286
🏁 Script executed:
#!/usr/bin/env bash
rg -nP 'prisma\.user\.findUnique' -C5 packages/trpc/server/middlewares/sessionMiddleware.tsLength of output: 93
🏁 Script executed:
#!/usr/bin/env bash
sed -n '1,200p' packages/trpc/server/middlewares/sessionMiddleware.tsLength of output: 5859
🏁 Script executed:
#!/usr/bin/env bash
rg -nP 'findUnlockedUserForSession' -C10 packages/lib/server/repository/user.tsLength of output: 709
🏁 Script executed:
#!/usr/bin/env bash
# Display full select block for findUnlockedUserForSession to confirm if notifyCalendarAlerts is selected
sed -n '860,930p' packages/lib/server/repository/user.tsLength of output: 1790
Select notifyCalendarAlerts in findUnlockedUserForSession
In packages/lib/server/repository/user.ts, the Prisma select block for findUnlockedUserForSession (around lines 863–897) omits notifyCalendarAlerts, so user.notifyCalendarAlerts will always be undefined. Add:
notifyCalendarAlerts: true,immediately after receiveMonthlyDigestEmail: true, in that select.
🤖 Prompt for AI Agents
In packages/trpc/server/routers/viewer/me/get.handler.ts around lines 143–144,
the handler spreads profileData and references user.notifyCalendarAlerts but the
Prisma select in packages/lib/server/repository/user.ts (around lines 863–897)
does not include notifyCalendarAlerts; open that select and add
notifyCalendarAlerts: true, immediately after receiveMonthlyDigestEmail: true,
so the field is returned and user.notifyCalendarAlerts is defined.
| console.log("🔧 Testing Calendar Notifications Feature"); | ||
| console.log("=========================================\n"); | ||
|
|
||
| // Test 1: Check if our new files exist |
There was a problem hiding this comment.
Make failures detectable and CI-friendly (collect failures automatically)
Right now, the script always exits 0 and later claims success regardless of misses. Intercept ❌ logs and count failures for summary/exitCode.
console.log("🔧 Testing Calendar Notifications Feature");
console.log("=========================================\n");
+// Count failures by intercepting lines that start with "❌"
+const __origLog = console.log;
+let __failures = 0;
+console.log = (...args) => {
+ if (typeof args[0] === "string" && args[0].trim().startsWith("❌")) __failures++;
+ __origLog(...args);
+};
+
// Test 1: Check if our new files exist📝 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.
| console.log("🔧 Testing Calendar Notifications Feature"); | |
| console.log("=========================================\n"); | |
| // Test 1: Check if our new files exist | |
| console.log("🔧 Testing Calendar Notifications Feature"); | |
| console.log("=========================================\n"); | |
| // Count failures by intercepting lines that start with "❌" | |
| const __origLog = console.log; | |
| let __failures = 0; | |
| console.log = (...args) => { | |
| if (typeof args[0] === "string" && args[0].trim().startsWith("❌")) __failures++; | |
| __origLog(...args); | |
| }; | |
| // Test 1: Check if our new files exist |
🤖 Prompt for AI Agents
In test-calendar-notifications.js around lines 13 to 16, the script currently
only prints headings and never reports failures to CI; modify the test harness
to intercept any failure logs (e.g., detect/replace console.error or look for
the "❌" marker), maintain a failure counter incremented whenever a failure is
emitted, and at the end print a concise summary with counts and call
process.exit(1) when failureCount > 0 (process.exit(0) otherwise) so CI can
detect non-zero exit on test failures.
| if (fs.existsSync(translationPath)) { | ||
| const content = fs.readFileSync(translationPath, "utf8"); | ||
|
|
||
| if (content.includes("calendar_notifications")) { | ||
| console.log("✅ Calendar notification translations added"); | ||
| } else { | ||
| console.log("❌ Calendar notification translations missing"); | ||
| } | ||
| } else { |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Parse translation JSON instead of substring match
Ensure the key truly exists and the JSON is valid.
if (fs.existsSync(translationPath)) {
const content = fs.readFileSync(translationPath, "utf8");
- if (content.includes("calendar_notifications")) {
- console.log("✅ Calendar notification translations added");
- } else {
- console.log("❌ Calendar notification translations missing");
- }
+ try {
+ const data = JSON.parse(content);
+ if (Object.prototype.hasOwnProperty.call(data, "calendar_notifications")) {
+ console.log("✅ Calendar notification translations added");
+ } else {
+ console.log("❌ Calendar notification translations missing");
+ }
+ } catch {
+ console.log("❌ Translation file is not valid JSON");
+ }
} else {
console.log("❌ Translation file not found");
}📝 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.
| if (fs.existsSync(translationPath)) { | |
| const content = fs.readFileSync(translationPath, "utf8"); | |
| if (content.includes("calendar_notifications")) { | |
| console.log("✅ Calendar notification translations added"); | |
| } else { | |
| console.log("❌ Calendar notification translations missing"); | |
| } | |
| } else { | |
| if (fs.existsSync(translationPath)) { | |
| const content = fs.readFileSync(translationPath, "utf8"); | |
| try { | |
| const data = JSON.parse(content); | |
| if (Object.prototype.hasOwnProperty.call(data, "calendar_notifications")) { | |
| console.log("✅ Calendar notification translations added"); | |
| } else { | |
| console.log("❌ Calendar notification translations missing"); | |
| } | |
| } catch { | |
| console.log("❌ Translation file is not valid JSON"); | |
| } | |
| } else { | |
| console.log("❌ Translation file not found"); | |
| } |
🤖 Prompt for AI Agents
In test-calendar-notifications.js around lines 95 to 103, replace the current
substring check of the translation file with proper JSON parsing and a key
existence check: read the file, wrap JSON.parse in a try/catch to fail/log an
invalid JSON error, then verify the parsed object has a top-level
"calendar_notifications" property (use Object.prototype.hasOwnProperty.call or
the "in" operator) and log success/failure accordingly; ensure any parse error
or missing key results in the "missing" log and does not silently pass.
|
@Vansh5632 pls attach a loom video |
|
okay i am working some changes are there after which i will put the video |
|
Making it draft. Feel free to rfr once it is ready. |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (3)
apps/web/components/apps/CalendarListContainer.tsx (3)
121-134: Use mutation onMutate for optimistic update + safe rollback (avoid stale closures).Current rollback flips state with
!isNotifyCalendarAlertsChecked, which can be wrong with rapid toggles. Use tRPC’sonMutateto stash prev state and revert from context.- const updateProfileMutation = trpc.viewer.me.updateProfile.useMutation({ - onSuccess: async () => { - showToast(t("settings_updated_successfully"), "success"); - setLoading(false); - // Don't invalidate immediately to avoid race conditions with optimistic updates - }, - onError: (error) => { - showToast(error.message, "error"); - // Revert the optimistic update on error - setIsNotifyCalendarAlertsChecked(!isNotifyCalendarAlertsChecked); - setLoading(false); - }, - }); + const updateProfileMutation = trpc.viewer.me.updateProfile.useMutation({ + onMutate: async (input) => { + setLoading(true); + const prev = isNotifyCalendarAlertsChecked; + setIsNotifyCalendarAlertsChecked(input.notifyCalendarAlerts ?? true); + return { prev }; + }, + onSuccess: async () => { + showToast(t("settings_updated_successfully"), "success"); + setLoading(false); + }, + onError: (err, _input, ctx) => { + showToast(err.message, "error"); + if (ctx?.prev !== undefined) setIsNotifyCalendarAlertsChecked(ctx.prev); + setLoading(false); + }, + }); - const handleCalendarNotificationToggle = async (enabled: boolean) => { - setLoading(true); - // Optimistic update - setIsNotifyCalendarAlertsChecked(enabled); - updateProfileMutation.mutate({ - notifyCalendarAlerts: enabled, - }); - }; + const handleCalendarNotificationToggle = (enabled: boolean) => { + updateProfileMutation.mutate({ notifyCalendarAlerts: enabled }); + };Also applies to: 135-142
127-133: Avoid name shadowing for “error”.The
onErrorparam name shadows theerrorfromuseRouterQuery, which harms readability. Rename toerr.
272-276: Add accessible name to the Switch.Provide an aria-label so screen readers announce the control properly.
<Switch checked={isNotifyCalendarAlertsChecked} onCheckedChange={handleCalendarNotificationToggle} disabled={loading} + aria-label={t("unreachable_calendar_alerts")} />
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (4)
apps/web/components/apps/CalendarListContainer.tsx(6 hunks)packages/lib/markCredentialAsUnreachable.ts(1 hunks)packages/lib/server/repository/credential.ts(1 hunks)packages/trpc/server/routers/viewer/me/get.handler.ts(2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- packages/trpc/server/routers/viewer/me/get.handler.ts
- packages/lib/markCredentialAsUnreachable.ts
🧰 Additional context used
📓 Path-based instructions (4)
**/*.tsx
📄 CodeRabbit inference engine (.cursor/rules/review.mdc)
Always use
t()for text localization in frontend code; direct text embedding should trigger a warning
Files:
apps/web/components/apps/CalendarListContainer.tsx
**/*.{ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/review.mdc)
Flag excessive Day.js use in performance-critical code; prefer native Date or Day.js
.utc()in hot paths like loops
Files:
apps/web/components/apps/CalendarListContainer.tsxpackages/lib/server/repository/credential.ts
**/*.{ts,tsx,js,jsx}
⚙️ CodeRabbit configuration file
Flag default exports and encourage named exports. Named exports provide better tree-shaking, easier refactoring, and clearer imports. Exempt main components like pages, layouts, and components that serve as the primary export of a module.
Files:
apps/web/components/apps/CalendarListContainer.tsxpackages/lib/server/repository/credential.ts
**/*.ts
📄 CodeRabbit inference engine (.cursor/rules/review.mdc)
**/*.ts: For Prisma queries, only select data you need; never useinclude, always useselect
Ensure thecredential.keyfield is never returned from tRPC endpoints or APIs
Files:
packages/lib/server/repository/credential.ts
🧠 Learnings (1)
📓 Common learnings
Learnt from: ShashwatPS
PR: calcom/cal.com#23638
File: packages/trpc/server/routers/viewer/calendars/setDestinationReminder.handler.test.ts:198-199
Timestamp: 2025-09-06T11:00:34.348Z
Learning: In calcom/cal.com PR #23638, the maintainer ShashwatPS determined that authorization checks in the setDestinationReminder handler are "not applicable" when CodeRabbit suggested adding user-scoped WHERE clauses to prevent users from modifying other users' destination calendar reminder settings.
⏰ Context from checks skipped due to timeout of 180000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Codacy Static Code Analysis
🔇 Additional comments (4)
packages/lib/server/repository/credential.ts (2)
27-28: LGTM — fields align with the new migration and Unreachable flow.
57-63: Verify “with key” accessor never crosses tRPC boundaries.Double-check that
findFirstByIdWithKeyAndUseris not used in any tRPC handler response paths.#!/bin/bash set -euo pipefail # Calls within tRPC server rg -n "findFirstByIdWithKeyAndUser" packages/trpc/server || true # Any tRPC selects that expose credential.key rg -nP 'select:\s*{[^}]*\bkey:\s*true\b' packages/trpc/server || trueapps/web/components/apps/CalendarListContainer.tsx (2)
195-204: Good i18n usage.All user-facing strings use t(); no hardcoded text.
111-120: Sanity-check upstream: me.get and updateProfile must include notifyCalendarAlerts.Confirm the server returns and accepts this field to avoid undefined defaults on first render.
#!/bin/bash set -euo pipefail # Returned from viewer.me.get rg -n "notifyCalendarAlerts" packages/trpc/server/routers/viewer/me/get.handler.ts || true # Accepted by viewer.me.updateProfile rg -n "notifyCalendarAlerts" packages/trpc/server/routers/viewer/me/updateProfile.schema.ts || true # Translation keys exist rg -n 'calendar_notifications|unreachable_calendar_alerts' apps/web/public/static/locales/en/common.json || true
| isUnreachable?: boolean; | ||
| lastNotified?: Date | null; | ||
| }; |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Guard writes to isUnreachable/lastNotified (reduce accidental toggles).
Widening the generic update input to include these safety-critical fields increases the risk of unintended state flips (could affect availability/overbooking semantics). Prefer a dedicated repository method to update only reachability fields, and keep the generic updater from touching them.
Apply this diff to narrow the generic updater’s input:
type CredentialUpdateInput = {
type?: string;
key?: any;
userId?: number;
appId?: string;
delegationCredentialId?: string | null;
invalid?: boolean;
- isUnreachable?: boolean;
- lastNotified?: Date | null;
};Add a focused updater (example outside the shown ranges):
// New dedicated method
static async updateReachabilityById({
id,
isUnreachable,
lastNotified,
}: { id: number; isUnreachable?: boolean; lastNotified?: Date | null }) {
return prisma.credential.update({
where: { id },
data: { isUnreachable, lastNotified },
});
}🤖 Prompt for AI Agents
In packages/lib/server/repository/credential.ts around lines 27 to 29, the
generic updater currently allows writes to isUnreachable and lastNotified which
are safety-critical; remove these fields from the generic update input type
(narrow the updater's allowed data so it cannot set isUnreachable or
lastNotified) and add a dedicated repository method updateReachabilityById that
accepts only { id, isUnreachable?, lastNotified? } and performs a targeted
update for those two fields using prisma.credential.update; ensure the generic
updater’s callers are updated to use the new method where they intend to change
reachability.
|
@Vansh5632 Please attach a loom. |
|
@kart1ka video Added |
The attached video does not quite work for us. That video does not show the complete feature actually working. |
Screencast.from.2025-09-09.22-03-18.mp4this is the more detailed video showing that the emails are actually reaching and shown in mail dev if needed i can push the test which i have used |
|
This PR is being marked as stale due to inactivity. |
pallava-joshi
left a comment
There was a problem hiding this comment.
can you resolve the merge conflicts. also we have some failing unit tests.
Devin AI is completing this stale PRThis PR by @Vansh5632 has been marked as stale. A Devin session has been created to complete the remaining work. Devin will review the PR, address any feedback, and push updates to complete this PR. |
Resolved merge conflicts in: - apps/web/components/apps/CalendarListContainer.tsx - packages/emails/email-manager.ts - packages/prisma/schema.prisma - packages/trpc/server/routers/viewer/me/get.handler.ts - packages/trpc/server/routers/viewer/me/updateProfile.schema.ts Co-Authored-By: unknown <>
…ions - Add HTML escaping to email template to prevent XSS - Change UnreachableCalendarEmail from default to named export - Update email-manager.ts import for named export - Improve CalendarListContainer.tsx with onMutate for optimistic updates - Use isLoading from trpc query for better UX - Add aria-label to Switch component for accessibility - Fix CredentialRepository import path in markCredentialAsUnreachable.ts Co-authored-by: Vansh5632 <vansh5632@users.noreply.github.com> Co-Authored-By: unknown <>
Summary of ChangesI've completed the work on this PR by addressing the merge conflicts and CodeRabbit review feedback: Merge Conflicts Resolved
CodeRabbit Review Feedback Addressed
The PR is now ready for review. All changes maintain the original author's intent while incorporating the suggested improvements. Co-authored-by: @Vansh5632 |
What does this PR do?
This PR implements a comprehensive Unreachable Calendar Notifications System that detects when connected calendars become unreachable and notifies users via email with clear explanations and recovery instructions, while maintaining Cal.com's safety-first approach against overbooking.
Updates since last revision
This PR was completed by addressing merge conflicts and CodeRabbit review feedback:
Merge Conflicts Resolved
apps/web/components/apps/CalendarListContainer.tsxpackages/emails/email-manager.tspackages/prisma/schema.prismapackages/trpc/server/routers/viewer/me/get.handler.tspackages/trpc/server/routers/viewer/me/updateProfile.schema.tsCodeRabbit Review Feedback Addressed
esc()method toUnreachableCalendarEmailclass to escape dynamic values preventing XSS vulnerabilitiesUnreachableCalendarEmailfrom default to named exportonMutatecallback for proper optimistic updates with rollbackisLoadingfrom tRPC query to disable Switch during loadingaria-labelto Switch componentCredentialRepositoryimport path inmarkCredentialAsUnreachable.tsImage
Video
Screencast.from.2025-09-08.16-25-24.mp4
Human Review Checklist
Mandatory Tasks (DO NOT REMOVE)
Link to Devin run: https://app.devin.ai/sessions/c46997ef286d4cbfa6d98dd863562fed
Requested by: unknown ()
Original author: @Vansh5632