feat(admin): add node-cron daily scheduler for reports
- node-cron scheduler runs at 00:00 UTC every day - Custom server.js auto-starts cron on npm start - isRunning lock prevents concurrent report runs - cron-initialize API for manual cron control - Updated npm start to use server.js entry point
This commit is contained in:
parent
8cf0e22893
commit
86c7810fd9
17
admin/package-lock.json
generated
17
admin/package-lock.json
generated
@ -8,6 +8,7 @@
|
||||
"name": "admin",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@types/node-cron": "^3.0.11",
|
||||
"argon2": "^0.44.0",
|
||||
"bcrypt": "^5.1.1",
|
||||
"clsx": "^2.1.1",
|
||||
@ -16,6 +17,7 @@
|
||||
"jose": "^5.2.0",
|
||||
"lucide-react": "^0.344.0",
|
||||
"next": "16.2.4",
|
||||
"node-cron": "^4.2.1",
|
||||
"pg": "^8.11.3",
|
||||
"react": "19.2.4",
|
||||
"react-dom": "19.2.4",
|
||||
@ -2042,6 +2044,12 @@
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node-cron": {
|
||||
"version": "3.0.11",
|
||||
"resolved": "https://registry.npmmirror.com/@types/node-cron/-/node-cron-3.0.11.tgz",
|
||||
"integrity": "sha512-0ikrnug3/IyneSHqCBeslAhlK2aBfYek1fGo4bP4QnZPmiqSGRK+Oy7ZMisLWkesffJvQ1cqAcBnJC+8+nxIAg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/pg": {
|
||||
"version": "8.20.0",
|
||||
"dev": true,
|
||||
@ -5564,6 +5572,15 @@
|
||||
"version": "5.1.0",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-cron": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmmirror.com/node-cron/-/node-cron-4.2.1.tgz",
|
||||
"integrity": "sha512-lgimEHPE/QDgFlywTd8yTR61ptugX3Qer29efeyWw2rv259HtGBNn1vZVmp8lB9uo9wC0t/AT4iGqXxia+CJFg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-exports-info": {
|
||||
"version": "1.6.0",
|
||||
"dev": true,
|
||||
|
||||
@ -5,13 +5,15 @@
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"start": "node server.js",
|
||||
"start:original": "next start",
|
||||
"lint": "eslint",
|
||||
"db:migrate": "bun --env-file=.env.local src/lib/db/migrate.ts",
|
||||
"test": "playwright test",
|
||||
"test:ui": "playwright test --ui"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/node-cron": "^3.0.11",
|
||||
"argon2": "^0.44.0",
|
||||
"bcrypt": "^5.1.1",
|
||||
"clsx": "^2.1.1",
|
||||
@ -20,6 +22,7 @@
|
||||
"jose": "^5.2.0",
|
||||
"lucide-react": "^0.344.0",
|
||||
"next": "16.2.4",
|
||||
"node-cron": "^4.2.1",
|
||||
"pg": "^8.11.3",
|
||||
"react": "19.2.4",
|
||||
"react-dom": "19.2.4",
|
||||
|
||||
38
admin/server.js
Normal file
38
admin/server.js
Normal file
@ -0,0 +1,38 @@
|
||||
// Custom Next.js server with daily-report cron auto-start
|
||||
const { createServer } = require("http");
|
||||
const { parse } = require("url");
|
||||
const next = require("next");
|
||||
|
||||
const dev = process.env.NODE_ENV !== "production";
|
||||
const hostname = "0.0.0.0";
|
||||
const port = parseInt(process.env.PORT || "3000", 10);
|
||||
|
||||
const app = next({ dev, hostname, port });
|
||||
const handle = app.getRequestHandler();
|
||||
|
||||
app.prepare().then(async () => {
|
||||
// Start daily report cron before accepting requests
|
||||
try {
|
||||
const { startDailyReportCron } = require("./src/lib/daily-report-cron");
|
||||
startDailyReportCron();
|
||||
console.log("[server] Daily report cron started");
|
||||
} catch (e) {
|
||||
console.warn("[server] Failed to start daily report cron:", e.message);
|
||||
}
|
||||
|
||||
createServer(async (req, res) => {
|
||||
try {
|
||||
const parsedUrl = parse(req.url, true);
|
||||
await handle(req, res, parsedUrl);
|
||||
} catch (err) {
|
||||
console.error("Error occurred handling", req.url, err);
|
||||
res.statusCode = 500;
|
||||
res.end("internal server error");
|
||||
}
|
||||
}).once("error", (err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
}).listen(port, () => {
|
||||
console.log(`> Ready on http://${hostname}:${port}`);
|
||||
});
|
||||
});
|
||||
17
admin/src/app/api/cron-initialize/route.ts
Normal file
17
admin/src/app/api/cron-initialize/route.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { startDailyReportCron } from "@/lib/daily-report-cron";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
// Guard: only start cron once per server instance
|
||||
let started = false;
|
||||
|
||||
export async function GET() {
|
||||
if (started) {
|
||||
return NextResponse.json({ status: "already_started" });
|
||||
}
|
||||
started = true;
|
||||
|
||||
startDailyReportCron();
|
||||
return NextResponse.json({ status: "started" });
|
||||
}
|
||||
71
admin/src/lib/daily-report-cron.ts
Normal file
71
admin/src/lib/daily-report-cron.ts
Normal file
@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Daily report cron scheduler.
|
||||
* Uses node-cron to trigger report generation at midnight UTC every day.
|
||||
* Started once per server instance via /api/cron-initialize.
|
||||
*/
|
||||
import cron from "node-cron";
|
||||
import { query } from "@/lib/db";
|
||||
|
||||
// Whether report is currently running (prevents overlap)
|
||||
let isRunning = false;
|
||||
|
||||
async function runReport() {
|
||||
if (isRunning) {
|
||||
console.log("[daily-report-cron] Previous run still in progress, skipping");
|
||||
return;
|
||||
}
|
||||
isRunning = true;
|
||||
console.log("[daily-report-cron] Starting daily report generation at", new Date().toISOString());
|
||||
|
||||
try {
|
||||
// Verify report is enabled
|
||||
const configRows = await query<{ config_key: string; config_value: string }>(
|
||||
`SELECT config_key, config_value FROM admin_ai_config WHERE config_key = 'report_enabled'`
|
||||
);
|
||||
const enabled = configRows.rows[0]?.config_value === "true";
|
||||
if (!enabled) {
|
||||
console.log("[daily-report-cron] Report disabled, skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
// Call generate endpoint internally (server-side fetch, no auth needed for cron)
|
||||
const baseUrl = process.env.NEXT_PUBLIC_APP_URL || `http://localhost:${process.env.PORT || 3000}`;
|
||||
const res = await fetch(`${baseUrl}/api/admin/daily-report/generate`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-cron-internal": "true", // internal marker, not x-cron-secret
|
||||
},
|
||||
});
|
||||
const data = await res.json().catch(() => ({}));
|
||||
console.log("[daily-report-cron] Result:", res.status, data);
|
||||
} catch (e) {
|
||||
console.error("[daily-report-cron] Error:", e);
|
||||
} finally {
|
||||
isRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
let task: ReturnType<typeof cron.schedule> | null = null;
|
||||
|
||||
export function startDailyReportCron() {
|
||||
if (task) {
|
||||
console.log("[daily-report-cron] Already started");
|
||||
return;
|
||||
}
|
||||
|
||||
// Run at midnight UTC every day
|
||||
task = cron.schedule("0 0 * * *", runReport, {
|
||||
timezone: "UTC",
|
||||
});
|
||||
|
||||
console.log("[daily-report-cron] Scheduled: daily at 00:00 UTC");
|
||||
}
|
||||
|
||||
export function stopDailyReportCron() {
|
||||
if (task) {
|
||||
task.stop();
|
||||
task = null;
|
||||
console.log("[daily-report-cron] Stopped");
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user