diff --git a/cron/cronJobDaily.sh b/cron/cronJobDaily.sh index f2b8bc3b..bef27620 100644 --- a/cron/cronJobDaily.sh +++ b/cron/cronJobDaily.sh @@ -3,4 +3,4 @@ SCRIPT_DIR="/var/www/html/STAPLE/cron" # Change to the script's directory cd "$SCRIPT_DIR" || exit # Run the Node.js scriptnode cronJob.mjs -node cronJobMailer.mjs +node cronJobDailyMailer.mjs diff --git a/cron/cronJobMailer.mjs b/cron/cronJobDailyMailer.mjs similarity index 71% rename from cron/cronJobMailer.mjs rename to cron/cronJobDailyMailer.mjs index c46cae4d..5df4bc42 100644 --- a/cron/cronJobMailer.mjs +++ b/cron/cronJobDailyMailer.mjs @@ -1,7 +1,7 @@ import dotenv from "dotenv" dotenv.config({ path: "../.env.local" }) import moment from "moment" -import { PrismaClient } from "@prisma/client" +import { PrismaClient, EmailFrequency } from "@prisma/client" import fetch from "node-fetch" import { resolver } from "@blitzjs/rpc" @@ -26,6 +26,7 @@ function createDailyNotification(email, notificationContent, overdueContent) {
This email is to notify you about overdue tasks and recent updates to your project(s). You can view all notifications on the Notifications page. + You change the frequency of these emails on your Profile page.
No new updates in the last 24 hours.
" + // If the user doesn't want any daily emails, skip them entirely + if (!wantsProjectDaily && !wantsOverdueDaily) { + console.log(`[Mailer] User ${email} has no DAILY email prefs, skipping in daily job.`) + continue + } - // Build overdue content for this recipient (if any) + const projects = groupedNotifications?.[email] || {} const overdueProjects = groupedOverdues?.[email] || {} - const overdueContent = - Object.entries(overdueProjects) - .map(([projectName, rows]) => { - const projectHeader = `No overdue tasks 🎉
" + + const hasProjectData = Object.keys(projects).length > 0 + const hasOverdueData = Object.keys(overdueProjects).length > 0 + + const willHaveProjectSection = wantsProjectDaily && hasProjectData + const willHaveOverdueSection = wantsOverdueDaily && hasOverdueData + + console.log( + `[Mailer][Daily] ${email} prefs: project=${user.emailProjectActivityFrequency}, overdue=${user.emailOverdueTaskFrequency}; ` + + `wantsProjectDaily=${wantsProjectDaily}, wantsOverdueDaily=${wantsOverdueDaily}; ` + + `hasProjectData=${hasProjectData}, hasOverdueData=${hasOverdueData}, ` + + `willHaveProjectSection=${willHaveProjectSection}, willHaveOverdueSection=${willHaveOverdueSection}` + ) + + // If there is nothing relevant to send for this cadence, skip + if (!willHaveProjectSection && !willHaveOverdueSection) { + console.log(`[Mailer] No relevant daily content for ${email} (prefs or data), skipping.`) + continue + } + + // Build project updates section + const notificationContent = !wantsProjectDaily + ? "You are not subscribed to daily project update emails.
" + : hasProjectData + ? Object.entries(projects) + .map(([projectName, messages]) => { + const projectHeader = `No new updates in the last 24 hours.
" + + // Build overdue tasks section + const overdueContent = !wantsOverdueDaily + ? "You are not subscribed to daily overdue task emails.
" + : hasOverdueData + ? Object.entries(overdueProjects) + .map(([projectName, rows]) => { + const projectHeader = `No overdue tasks 🎉
" const emailContent = createDailyNotification(email, notificationContent, overdueContent) console.log( - `[Mailer] Prepared email for ${email}: hasOverdues=${!!Object.keys(overdueProjects) - .length}, hasUpdates=${!!Object.keys(projects).length}` + `[Mailer] Prepared daily email for ${email}: hasOverdues=${willHaveOverdueSection}, hasUpdates=${willHaveProjectSection}` ) // Check rate limit before sending email diff --git a/cron/cronJobDeleteTmp.mjs b/cron/cronJobDeleteTmp.mjs index f782b722..c21695b4 100644 --- a/cron/cronJobDeleteTmp.mjs +++ b/cron/cronJobDeleteTmp.mjs @@ -1,8 +1,12 @@ import fs from "fs" import path from "path" +import { fileURLToPath } from "url" + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) function cleanUpViewerZips() { - const zipDir = path.join(process.cwd(), "viewer-builds") + const zipDir = path.join(__dirname, "..", "viewer-builds") if (!fs.existsSync(zipDir)) return const files = fs.readdirSync(zipDir) diff --git a/cron/cronJobFolderSize.mjs b/cron/cronJobFolderSize.mjs index 834be149..b2f10085 100644 --- a/cron/cronJobFolderSize.mjs +++ b/cron/cronJobFolderSize.mjs @@ -1,8 +1,12 @@ import fs from "fs" import path from "path" +import { fileURLToPath } from "url" + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) function checkViewerBuildsSize() { - const zipDir = path.join(process.cwd(), "viewer-builds") + const zipDir = path.join(__dirname, "..", "viewer-builds") if (!fs.existsSync(zipDir)) return const files = fs.readdirSync(zipDir) diff --git a/cron/cronJobWeekly.sh b/cron/cronJobWeekly.sh new file mode 100644 index 00000000..c48a4b64 --- /dev/null +++ b/cron/cronJobWeekly.sh @@ -0,0 +1,6 @@ +# Absolute path to the directory where your cronJob.mjs is located +SCRIPT_DIR="/var/www/html/STAPLE/cron" +# Change to the script's directory +cd "$SCRIPT_DIR" || exit +# Run the Node.js scriptnode cronJob.mjs +node cronJobWeeklyMailer.mjs diff --git a/cron/cronJobWeeklyMailer.mjs b/cron/cronJobWeeklyMailer.mjs new file mode 100644 index 00000000..022040d6 --- /dev/null +++ b/cron/cronJobWeeklyMailer.mjs @@ -0,0 +1,333 @@ +import dotenv from "dotenv" +dotenv.config({ path: "../.env.local" }) +import moment from "moment" +import { PrismaClient, EmailFrequency } from "@prisma/client" +import fetch from "node-fetch" +import { resolver } from "@blitzjs/rpc" + +const db = new PrismaClient() // Create Prisma client instance + +function fmtDate(date) { + return moment(date).format("MMM D, YYYY") +} + +// Helper function to create email content +function createWeeklyNotification(email, notificationContent, overdueContent) { + const html_message = ` + + +
+ + This email is to notify you about overdue tasks and updates to your project(s) from the last week. + You can view all notifications on the Notifications page. + You change the frequency of these emails on your Profile page. +
+ +No new updates in the last week.
" + : "You are not subscribed to weekly project update emails.
" + + // Build overdue tasks section + const overdueContent = willHaveOverdueSection + ? Object.entries(overdueProjects) + .map(([projectName, rows]) => { + const projectHeader = `No overdue tasks 🎉
" + : "You are not subscribed to weekly overdue task emails.
" + + const emailContent = createWeeklyNotification(email, notificationContent, overdueContent) + + console.log( + `[Mailer] Prepared weekly email for ${email}: hasOverdues=${willHaveOverdueSection}, hasUpdates=${willHaveProjectSection}` + ) + + // Check rate limit before sending email + await checkRateLimit() + + // Send email + try { + const response = await fetch("https://app.staple.science/api/send-email", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(emailContent), + }) + + const respText = await response.text().catch(() => "If you enter your first and last name, it will replace your username in project areas. diff --git a/src/users/mutations/updateUser.ts b/src/users/mutations/updateUser.ts index 6fb95a98..9ce090a1 100644 --- a/src/users/mutations/updateUser.ts +++ b/src/users/mutations/updateUser.ts @@ -7,7 +7,18 @@ export default resolver.pipe( resolver.zod(UpdateUserSchema), resolver.authorize(), async ( - { email, firstName, lastName, institution, username, language, gravatar, tooltips }, + { + email, + firstName, + lastName, + institution, + username, + language, + gravatar, + tooltips, + emailProjectActivityFrequency, + emailOverdueTaskFrequency, + }, ctx: Ctx ) => { const user = await db.user.findFirst({ where: { id: ctx.session.userId! } }) @@ -25,6 +36,8 @@ export default resolver.pipe( language: language, gravatar: gravatar, tooltips: tooltips, + emailProjectActivityFrequency: emailProjectActivityFrequency, + emailOverdueTaskFrequency: emailOverdueTaskFrequency, }, }) diff --git a/src/users/queries/getCurrentUser.ts b/src/users/queries/getCurrentUser.ts index d7ea178e..86859e2c 100644 --- a/src/users/queries/getCurrentUser.ts +++ b/src/users/queries/getCurrentUser.ts @@ -14,6 +14,8 @@ export type CurrentUser = Prisma.UserGetPayload<{ language: true gravatar: true tooltips: true + emailProjectActivityFrequency: true + emailOverdueTaskFrequency: true } }> @@ -37,6 +39,8 @@ export default async function getCurrentUser( language: true, gravatar: true, tooltips: true, + emailProjectActivityFrequency: true, + emailOverdueTaskFrequency: true, }, }) diff --git a/src/users/schemas.ts b/src/users/schemas.ts index fb92e859..102b7679 100644 --- a/src/users/schemas.ts +++ b/src/users/schemas.ts @@ -1,4 +1,5 @@ import { z } from "zod" +import { EmailFrequency } from "db" export const email = z .string() @@ -18,6 +19,8 @@ export const FormProfileSchema = z.object({ .transform((str) => str.toLowerCase().trim()) .nullable(), tooltips: z.boolean(), + emailProjectActivityFrequency: z.nativeEnum(EmailFrequency), + emailOverdueTaskFrequency: z.nativeEnum(EmailFrequency), }) export const UpdateUserSchema = z.object({ @@ -33,4 +36,6 @@ export const UpdateUserSchema = z.object({ .transform((str) => str.toLowerCase().trim()) .nullable(), tooltips: z.boolean(), + emailProjectActivityFrequency: z.nativeEnum(EmailFrequency), + emailOverdueTaskFrequency: z.nativeEnum(EmailFrequency), })