mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
ready to test
Testing Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
/***********************************************************************************************************************************************************************
|
||||
* File Name : admin.cron.js
|
||||
* Type : Cron Registry — Admin side
|
||||
* Description : Aggregates and registers every admin-facing scheduled job.
|
||||
* Each job module in cron/jobs/ exports { name, schedule, run }:
|
||||
* name {string} – unique identifier, used in logs
|
||||
* schedule {string} – standard cron expression (node-cron)
|
||||
* run {function} – async () => void, the actual work
|
||||
*
|
||||
* To add a new admin-side cron job:
|
||||
* 1. Create cron/jobs/yourJob.cron.js exporting
|
||||
* { name, schedule, run }
|
||||
* 2. require() it below and add it to the `jobs` array
|
||||
* That's the only wiring required — server.js never needs to
|
||||
* change when admin-side jobs are added/removed.
|
||||
*
|
||||
* Currently registered:
|
||||
* - taskOverdue (cron/jobs/taskOverdue.cron.js)
|
||||
*
|
||||
* Author: Kenneth Obsequio (@lash0000)
|
||||
* Date Created: Jun. 17, 2026
|
||||
***********************************************************************************************************************************************************************/
|
||||
const cron = require('node-cron');
|
||||
const taskOverdue = require('./jobs/task_overdue.cron');
|
||||
|
||||
// ─── Registry — add future admin-side cron jobs here ─────────────────────────
|
||||
const jobs = [
|
||||
taskOverdue,
|
||||
];
|
||||
|
||||
// ─── Boot all registered admin-side jobs ──────────────────────────────────────
|
||||
function startAdminCronJobs() {
|
||||
const registered = [];
|
||||
jobs.forEach(({ name, schedule, run }) => {
|
||||
if (!cron.validate(schedule)) {
|
||||
console.error(`[CRON][ADMIN] Invalid schedule for "${name}": "${schedule}" — skipped.`);
|
||||
return;
|
||||
}
|
||||
cron.schedule(schedule, run);
|
||||
registered.push({ name, scope: 'ADMIN', schedule });
|
||||
});
|
||||
return registered;
|
||||
}
|
||||
|
||||
module.exports = { startAdminCronJobs };
|
||||
@@ -0,0 +1,36 @@
|
||||
/***********************************************************************************************************************************************************************
|
||||
* File Name : client.cron.js
|
||||
* Type : Cron Registry — Client side
|
||||
* Description : Aggregates and registers every client-facing scheduled job.
|
||||
* Same shape as admin.cron.js — each job module exports
|
||||
* { name, schedule, run }, listed in the `jobs` array below.
|
||||
*
|
||||
* Currently registered:
|
||||
* - userNotifications (cron/jobs/user_notifications.cron.js)
|
||||
*
|
||||
* Author: Kenneth Obsequio (@lash0000)
|
||||
* Date Created: Jun. 17, 2026
|
||||
***********************************************************************************************************************************************************************/
|
||||
const cron = require('node-cron');
|
||||
const userNotifications = require('./jobs/user_notifications.cron');
|
||||
|
||||
// ─── Registry — add future client-side cron jobs here ────────────────────────
|
||||
const jobs = [
|
||||
userNotifications,
|
||||
];
|
||||
|
||||
// ─── Boot all registered client-side jobs ─────────────────────────────────────
|
||||
function startClientCronJobs() {
|
||||
const registered = [];
|
||||
jobs.forEach(({ name, schedule, run }) => {
|
||||
if (!cron.validate(schedule)) {
|
||||
console.error(`[CRON][CLIENT] Invalid schedule for "${name}": "${schedule}" — skipped.`);
|
||||
return;
|
||||
}
|
||||
cron.schedule(schedule, run);
|
||||
registered.push({ name, scope: 'CLIENT', schedule });
|
||||
});
|
||||
return registered;
|
||||
}
|
||||
|
||||
module.exports = { startClientCronJobs };
|
||||
@@ -0,0 +1,68 @@
|
||||
/***********************************************************************************************************************************************************************
|
||||
* File Name : task_overdue.cron.js
|
||||
* Type : Cron Job
|
||||
* Description : Flips Task.status to 'overdue' once its deadline has passed,
|
||||
* provided it isn't already 'completed' or 'overdue'. Purely
|
||||
* an admin-facing lifecycle label — does NOT touch
|
||||
* TaskCompletion/TaskLinkVisit/TaskProgress, does NOT affect
|
||||
* per-user completion signals or client-side Ongoing/Done/
|
||||
* Overdue bucketing, and does NOT block late submissions.
|
||||
*
|
||||
* Safety pattern:
|
||||
* - Task.update is the primary operation and must always succeed.
|
||||
* - AdminNotification.create is secondary — wrapped in its own
|
||||
* try/catch so a notification failure never rolls back or
|
||||
* suppresses the status flip. If it fails once, the next
|
||||
* hourly run will insert its own summary for that batch.
|
||||
*
|
||||
* Schedule : Every hour, on the hour ("0 * * * *"). Registered by
|
||||
* cron/admin.cron.js, not scheduled here directly.
|
||||
*
|
||||
* Author: Kenneth Obsequio (@lash0000)
|
||||
* Date Created: Jun. 17, 2026
|
||||
* Modified : Jun. 19, 2026
|
||||
***********************************************************************************************************************************************************************/
|
||||
const { Op } = require('sequelize');
|
||||
const { Task } = require('../../models/task/task.mdl');
|
||||
const AdminNotification = require('../../models/notifications/admin_notification.mdl');
|
||||
const { NOTIFICATION_REGISTRY } = require('../../data/notifications.data');
|
||||
|
||||
// ─── The actual sweep ────────────────────────────────────────────────────────
|
||||
async function run() {
|
||||
// ── 1. Primary: flip task statuses ───────────────────────────────────────
|
||||
let affectedCount = 0;
|
||||
|
||||
try {
|
||||
[affectedCount] = await Task.update(
|
||||
{ status: 'overdue' },
|
||||
{
|
||||
where: {
|
||||
deadline: { [Op.lt]: new Date() },
|
||||
status: { [Op.notIn]: ['completed', 'overdue'] },
|
||||
},
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
console.error('[CRON][TASK OVERDUE] Failed to update task statuses:', err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (affectedCount === 0) return;
|
||||
|
||||
console.log(`[CRON][TASK OVERDUE] Marked ${affectedCount} task(s) as overdue.`);
|
||||
|
||||
// ── 2. Secondary: admin notification — isolated, never blocks step 1 ─────
|
||||
try {
|
||||
await AdminNotification.create(
|
||||
NOTIFICATION_REGISTRY.task_overdue.build({ count: affectedCount })
|
||||
);
|
||||
} catch (err) {
|
||||
console.error('[CRON][TASK OVERDUE] Failed to insert admin notification:', err);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
name: 'taskOverdue',
|
||||
schedule: '0 * * * *',
|
||||
run,
|
||||
};
|
||||
@@ -0,0 +1,92 @@
|
||||
/***********************************************************************************************************************************************************************
|
||||
* File Name : user_task_overdue_notify.cron.js
|
||||
* Type : Cron Job
|
||||
* Description : Emits a UserNotification for every user who belongs to a group
|
||||
* assigned to a task list that contains a task that JUST flipped
|
||||
* to 'overdue' in the last hour.
|
||||
*
|
||||
* Runs 5 minutes after the admin taskOverdue cron (which fires at
|
||||
* the top of each hour) so the status flips are already committed
|
||||
* before this job queries them.
|
||||
*
|
||||
* "Just flipped" = status is 'overdue' AND updatedAt is within
|
||||
* the last 65 minutes (1-hour window + 5-min drift buffer).
|
||||
* This prevents re-notifying users for tasks that were already
|
||||
* overdue before this run.
|
||||
*
|
||||
* Schedule : 5 minutes past every hour ("5 * * * *"). Registered by
|
||||
* cron/client.cron.js.
|
||||
*
|
||||
* Author: Kenneth Obsequio (@lash0000)
|
||||
* Date Created: Jun. 19, 2026
|
||||
***********************************************************************************************************************************************************************/
|
||||
const { Op, QueryTypes } = require('sequelize');
|
||||
const sequelize = require('../../config/db.config');
|
||||
const { Task } = require('../../models/task/task.mdl');
|
||||
const UserNotification = require('../../models/notifications/user_notification.mdl');
|
||||
const { NOTIFICATION_REGISTRY } = require('../../data/notifications.data');
|
||||
|
||||
const WINDOW_MS = 65 * 60 * 1000; // 65-minute lookback
|
||||
|
||||
async function run() {
|
||||
// ── 1. Find tasks that flipped to overdue in the last 65 minutes ──────────
|
||||
let recentlyOverdue;
|
||||
try {
|
||||
recentlyOverdue = await Task.findAll({
|
||||
attributes: ['task_id', 'task_list_id', 'name'],
|
||||
where: {
|
||||
status: 'overdue',
|
||||
updatedAt: { [Op.gte]: new Date(Date.now() - WINDOW_MS) },
|
||||
},
|
||||
raw: true,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('[CRON][USER NOTIFY] Failed to query recently overdue tasks:', err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (recentlyOverdue.length === 0) return;
|
||||
|
||||
console.log(`[CRON][USER NOTIFY] ${recentlyOverdue.length} recently overdue task(s) — resolving affected users.`);
|
||||
|
||||
// ── 2. Resolve affected users via task_list_groups → user_group_members ───
|
||||
try {
|
||||
const taskListIds = [...new Set(recentlyOverdue.map(t => t.task_list_id))];
|
||||
|
||||
const affectedUsers = await sequelize.query(
|
||||
`SELECT DISTINCT ugm.user_id
|
||||
FROM task_list_groups tlg
|
||||
JOIN user_group_members ugm ON ugm.group_id = tlg.group_id
|
||||
AND ugm."deletedAt" IS NULL
|
||||
WHERE tlg.task_list_id IN (:taskListIds)`,
|
||||
{ replacements: { taskListIds }, type: QueryTypes.SELECT }
|
||||
);
|
||||
|
||||
if (affectedUsers.length === 0) return;
|
||||
|
||||
const count = recentlyOverdue.length;
|
||||
const now = new Date();
|
||||
const notify = NOTIFICATION_REGISTRY.user_task_overdue.build({ count, task_list_ids: taskListIds });
|
||||
|
||||
await UserNotification.bulkCreate(
|
||||
affectedUsers.map(({ user_id }) => ({
|
||||
user_id,
|
||||
...notify,
|
||||
seen: false,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
})),
|
||||
{ validate: false }
|
||||
);
|
||||
|
||||
console.log(`[CRON][USER NOTIFY] Notified ${affectedUsers.length} user(s) about ${count} overdue task(s).`);
|
||||
} catch (err) {
|
||||
console.error('[CRON][USER NOTIFY] Failed to emit user notifications:', err);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
name: 'userNotifications',
|
||||
schedule: '5 * * * *',
|
||||
run,
|
||||
};
|
||||
Reference in New Issue
Block a user