Add notification scheduler worker

This commit is contained in:
zv
2026-06-12 18:46:25 +02:00
parent 49265e7c51
commit fd2e108205
5 changed files with 103 additions and 5 deletions

View File

@@ -0,0 +1,84 @@
const DEFAULT_APP_URL = "http://127.0.0.1:3000";
const DEFAULT_WARNING_INTERVAL_MINUTES = 5;
const DEFAULT_MORNING_BRIEF_TIME = "07:00";
const LOOP_INTERVAL_MS = 30_000;
const appUrl = normalizeAppUrl(process.env.WTR_APP_URL ?? process.env.NEXT_PUBLIC_APP_URL ?? DEFAULT_APP_URL);
const cronSecret = process.env.NOTIFICATIONS_CRON_SECRET ?? "";
const warningIntervalMinutes = readPositiveNumber(process.env.NOTIFICATIONS_WARNING_INTERVAL_MINUTES, DEFAULT_WARNING_INTERVAL_MINUTES);
const morningBriefTime = normalizeTime(process.env.NOTIFICATIONS_MORNING_BRIEF_TIME ?? DEFAULT_MORNING_BRIEF_TIME);
let lastWarningCheckAt = 0;
let lastMorningBriefDate = "";
let isRunningTick = false;
function normalizeAppUrl(value) {
return value.replace(/\/+$/, "");
}
function readPositiveNumber(value, fallback) {
const number = Number(value);
return Number.isFinite(number) && number > 0 ? number : fallback;
}
function normalizeTime(value) {
return /^\d{2}:\d{2}$/.test(value) ? value : DEFAULT_MORNING_BRIEF_TIME;
}
function getWarsawDateParts(date = new Date()) {
const parts = new Intl.DateTimeFormat("en-CA", {
timeZone: "Europe/Warsaw",
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
hourCycle: "h23",
}).formatToParts(date);
const part = (type) => parts.find((entry) => entry.type === type)?.value ?? "";
return {
dateKey: `${part("year")}-${part("month")}-${part("day")}`,
time: `${part("hour")}:${part("minute")}`,
};
}
function isAtOrAfterTime(currentTime, targetTime) {
return currentTime >= targetTime;
}
async function callNotificationEndpoint(path) {
const response = await fetch(`${appUrl}${path}`, {
headers: cronSecret ? { authorization: `Bearer ${cronSecret}` } : {},
});
const body = await response.text();
if (!response.ok) throw new Error(`${path} returned ${response.status}: ${body}`);
return body;
}
async function tick() {
if (isRunningTick) return;
isRunningTick = true;
try {
const now = Date.now();
const warningIntervalMs = warningIntervalMinutes * 60_000;
if (now - lastWarningCheckAt >= warningIntervalMs) {
lastWarningCheckAt = now;
await callNotificationEndpoint("/api/notifications/check");
}
const warsawTime = getWarsawDateParts();
if (isAtOrAfterTime(warsawTime.time, morningBriefTime) && lastMorningBriefDate !== warsawTime.dateKey) {
await callNotificationEndpoint("/api/notifications/daily-brief");
lastMorningBriefDate = warsawTime.dateKey;
}
} catch (error) {
console.error(error);
} finally {
isRunningTick = false;
}
}
console.log(`wtr. notification worker: ${appUrl}`);
console.log(`Warnings every ${warningIntervalMinutes} min, morning brief at ${morningBriefTime} Europe/Warsaw.`);
void tick();
setInterval(tick, LOOP_INTERVAL_MS);