91 lines
3.1 KiB
JavaScript
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;
|
|
}
|
|
}
|