Files
redlight/server/jobs/calendarReminders.js
Michelle 8823f8789e
Some checks failed
Build & Push Docker Image / build (push) Has been cancelled
feat(calendar): add reminder functionality for events with notifications
2026-03-04 10:18:43 +01:00

91 lines
3.1 KiB
JavaScript

import { getDb } from '../config/database.js';
import { log } from '../config/logger.js';
import { createNotification } from '../config/notifications.js';
const CHECK_INTERVAL_MS = 60_000; // every minute
let timer = null;
/**
* Check for upcoming calendar events that need a reminder notification fired.
* Runs every minute. Updates `reminder_sent_at` after firing so reminders
* are never sent twice. Also resets `reminder_sent_at` to NULL when
* start_time or reminder_minutes is changed (handled in calendar route).
*/
async function runCheck() {
try {
const db = getDb();
// Fetch all events that have a reminder configured and haven't been sent yet
const pending = await db.all(`
SELECT ce.id, ce.uid, ce.title, ce.start_time, ce.reminder_minutes, ce.user_id,
ce.room_uid, ce.color
FROM calendar_events ce
WHERE ce.reminder_minutes IS NOT NULL
AND ce.reminder_sent_at IS NULL
`);
if (pending.length === 0) return;
const now = new Date();
const toFire = pending.filter(ev => {
const start = new Date(ev.start_time);
// Don't fire reminders for events that started more than 10 minutes ago (server downtime tolerance)
if (start < new Date(now.getTime() - 10 * 60_000)) return false;
const reminderTime = new Date(start.getTime() - ev.reminder_minutes * 60_000);
return reminderTime <= now;
});
for (const ev of toFire) {
try {
// Mark as sent immediately to prevent double-fire even if notification creation fails
await db.run(
'UPDATE calendar_events SET reminder_sent_at = ? WHERE id = ?',
[now.toISOString(), ev.id],
);
const start = new Date(ev.start_time);
const timeStr = start.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
const dateStr = start.toLocaleDateString([], { day: 'numeric', month: 'short' });
const body = `${dateStr} · ${timeStr}`;
const link = '/calendar';
// Notify the owner
await createNotification(ev.user_id, 'calendar_reminder', ev.title, body, link);
// Notify all accepted share users as well
const shares = await db.all(
'SELECT user_id FROM calendar_event_shares WHERE event_id = ?',
[ev.id],
);
for (const { user_id } of shares) {
await createNotification(user_id, 'calendar_reminder', ev.title, body, link);
}
log.server.info(`Calendar reminder fired for event ${ev.uid} (id=${ev.id})`);
} catch (evErr) {
log.server.error(`Calendar reminder failed for event ${ev.id}: ${evErr.message}`);
}
}
} catch (err) {
log.server.error(`Calendar reminder job error: ${err.message}`);
}
}
export function startCalendarReminders() {
if (timer) return;
// Slight delay on startup so DB is fully ready
setTimeout(() => {
runCheck();
timer = setInterval(runCheck, CHECK_INTERVAL_MS);
}, 5_000);
log.server.info('Calendar reminder job started');
}
export function stopCalendarReminders() {
if (timer) {
clearInterval(timer);
timer = null;
}
}