Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ STAPLE.Rproj
db/migrations/local/
db/migrations/
tmp/*
summary-viewer/docs/.nojekyll
summary-viewer/docs/*
viewer-builds/*
summary-viewer/src/data/project_summary.json

# ignore builds but keep the folder for folks
viewer-builds/*.zip
viewer-builds/Proj*
viewer-builds/viewer*
41 changes: 41 additions & 0 deletions CITATION.cff
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
cff-version: 1.2.0
message: "If you use STAPLE in your research, please cite it using the following information."
title: "STAPLE: Science Tracking Across the Project Lifespan"
doi: "10.5281/zenodo.13916969"
authors:
- family-names: "Buchanan"
given-names: "Erin M."
orcid: "https://orcid.org/0000-0002-9689-4189"
- family-names: "Kovacs"
given-names: "Marton"
orcid: "https://orcid.org/0000-0002-8142-8492"
- family-names: "Yedra"
given-names: "Engerst"
orcid: "https://orcid.org/0000-0002-9555-7148"
# Add more authors here, e.g.:
# - family-names: "Doe"
# given-names: "Jane"
# orcid: "https://orcid.org/XXXX-XXXX-XXXX-XXXX"

preferred-citation:
type: software
title: "STAPLE: Science Tracking Across the Project Lifespan"
authors:
- family-names: "Buchanan"
given-names: "Erin M."
orcid: "https://orcid.org/0000-0002-9689-4189"
- family-names: "Kovacs"
given-names: "Marton"
orcid: "https://orcid.org/0000-0002-8142-8492"
- family-names: "Yedra"
given-names: "Engerst"
orcid: "https://orcid.org/0000-0002-9555-7148"
url: "https://github.com/STAPLE/STAPLE"

contributors:
- family-names: "Hartgerink"
given-names: "Chris"
orcid: "https://orcid.org/0000-0003-1050-6809"
- family-names: "Sunami"
given-names: "Nami"
orcid: "https://orcid.org/0000-0001-5482-8370"
2 changes: 1 addition & 1 deletion cron/cronJobDaily.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
97 changes: 74 additions & 23 deletions cron/cronJobMailer.mjs → cron/cronJobDailyMailer.mjs
Original file line number Diff line number Diff line change
@@ -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"

Expand All @@ -26,6 +26,7 @@ function createDailyNotification(email, notificationContent, overdueContent) {
<p>
This email is to notify you about overdue tasks and recent updates to your project(s).
You can view all notifications on the <a href="https://app.staple.science/auth/login?next=%2Fnotifications">Notifications page</a>.
You change the frequency of these emails on your <a href="https://app.staple.science/auth/login?next=%2Fprofile">Profile page</a>.
</p>

<h3>⏰ Overdue Tasks</h3>
Expand Down Expand Up @@ -191,43 +192,93 @@ const checkRateLimit = async () => {
}
}
}
// Function to send grouped notifications
// Function to send grouped notifications (daily), respecting user email preferences
export async function sendGroupedNotifications(groupedNotifications, groupedOverdues) {
const delayTime = 500 // Delay time between each email in milliseconds (e.g., 1 second)
const delayTime = 500 // Delay time between each email in milliseconds

const allEmails = new Set([
...Object.keys(groupedNotifications || {}),
...Object.keys(groupedOverdues || {}),
])

for (const email of allEmails) {
const projects = groupedNotifications?.[email] || {}
// Look up the user to read their email frequency preferences
const user = await db.user.findUnique({
where: { email },
select: {
id: true,
emailProjectActivityFrequency: true,
emailOverdueTaskFrequency: true,
},
})

if (!user) {
console.log(`[Mailer] No user found for email ${email}, skipping.`)
continue
}

// This is the DAILY mailer, so only honor DAILY preferences here
const wantsProjectDaily = user.emailProjectActivityFrequency === EmailFrequency.DAILY
const wantsOverdueDaily = user.emailOverdueTaskFrequency === EmailFrequency.DAILY

const notificationContent =
Object.entries(projects)
.map(([projectName, messages]) => {
const projectHeader = `<h4>Project: ${projectName}</h4>`
const messagesList = messages.map((message) => `<li>${message}</li>`).join("")
return projectHeader + `<ul>${messagesList}</ul>`
})
.join("") || "<p>No new updates in the last 24 hours.</p>"
// 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 = `<h4>Project: ${projectName}</h4>`
const items = rows.map((row) => `<li>${row}</li>`).join("")
return projectHeader + `<ul>${items}</ul>`
})
.join("") || "<p>No overdue tasks 🎉</p>"

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
? "<p>You are not subscribed to daily project update emails.</p>"
: hasProjectData
? Object.entries(projects)
.map(([projectName, messages]) => {
const projectHeader = `<h4>Project: ${projectName}</h4>`
const messagesList = messages.map((message) => `<li>${message}</li>`).join("")
return projectHeader + `<ul>${messagesList}</ul>`
})
.join("")
: "<p>No new updates in the last 24 hours.</p>"

// Build overdue tasks section
const overdueContent = !wantsOverdueDaily
? "<p>You are not subscribed to daily overdue task emails.</p>"
: hasOverdueData
? Object.entries(overdueProjects)
.map(([projectName, rows]) => {
const projectHeader = `<h4>Project: ${projectName}</h4>`
const items = rows.map((row) => `<li>${row}</li>`).join("")
return projectHeader + `<ul>${items}</ul>`
})
.join("")
: "<p>No overdue tasks 🎉</p>"

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
Expand Down
6 changes: 5 additions & 1 deletion cron/cronJobDeleteTmp.mjs
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
6 changes: 5 additions & 1 deletion cron/cronJobFolderSize.mjs
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
6 changes: 6 additions & 0 deletions cron/cronJobWeekly.sh
Original file line number Diff line number Diff line change
@@ -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
Loading