← back to Norma
wire Morning Batch + weekly_intelligence to PM2 cron — fixes 77d/72d stale tables
36d0cb1574a8fd12a80db3ab7ee9712bf5ba01af · 2026-05-28 17:08:16 -0700 · Steve Abrams
Both writers existed but had no scheduled trigger:
- /api/cron/daily (Morning Batch / sessions table) was never hit on prod
- scripts/compute-intelligence.ts (weekly_intelligence table) wasn't in any
cron or ecosystem entry despite header saying 'Nightly cron script'
Adds:
- scripts/daily-cron.js: POSTs to /api/cron/daily with x-cron-secret (mirrors
credential-health-cron.js pattern)
- ecosystem.config.js: norma-morning-batch (cron 0 14 * * * = 6 AM PT)
- ecosystem.config.js: norma-intelligence (cron 0 8 * * * = midnight PT)
Both load .env.local via --env-file so CRON_SECRET/GEMINI_API_KEY reach the
procs. Surfaced by yolo task 49.
Files touched
M ecosystem.config.jsA scripts/daily-cron.js
Diff
commit 36d0cb1574a8fd12a80db3ab7ee9712bf5ba01af
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu May 28 17:08:16 2026 -0700
wire Morning Batch + weekly_intelligence to PM2 cron — fixes 77d/72d stale tables
Both writers existed but had no scheduled trigger:
- /api/cron/daily (Morning Batch / sessions table) was never hit on prod
- scripts/compute-intelligence.ts (weekly_intelligence table) wasn't in any
cron or ecosystem entry despite header saying 'Nightly cron script'
Adds:
- scripts/daily-cron.js: POSTs to /api/cron/daily with x-cron-secret (mirrors
credential-health-cron.js pattern)
- ecosystem.config.js: norma-morning-batch (cron 0 14 * * * = 6 AM PT)
- ecosystem.config.js: norma-intelligence (cron 0 8 * * * = midnight PT)
Both load .env.local via --env-file so CRON_SECRET/GEMINI_API_KEY reach the
procs. Surfaced by yolo task 49.
---
ecosystem.config.js | 37 +++++++++++++++++++++++++++++++++
scripts/daily-cron.js | 57 +++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 94 insertions(+)
diff --git a/ecosystem.config.js b/ecosystem.config.js
index b4a96c1..23fdc82 100644
--- a/ecosystem.config.js
+++ b/ecosystem.config.js
@@ -47,5 +47,42 @@ module.exports = {
NORMA_URL: 'http://127.0.0.1:7400',
},
},
+ {
+ // 2026-05-28 wire-up — Morning Batch was never running on prod (sessions
+ // table 77d stale per yolo task 49). /api/cron/daily wants x-cron-secret
+ // header; the wrapper supplies it from CRON_SECRET via --env-file.
+ name: 'norma-morning-batch',
+ script: 'scripts/daily-cron.js',
+ cwd: '/root/Projects/Norma',
+ interpreter: 'node',
+ interpreter_args: '--env-file=/root/Projects/Norma/.env.local',
+ // 14:00 UTC = 6:00 AM Pacific Time — matches the route header's
+ // documented cadence ("Called by system cron at 6:00 AM PT daily")
+ cron_restart: '0 14 * * *',
+ autorestart: false,
+ watch: false,
+ env: {
+ NODE_ENV: 'production',
+ NORMA_URL: 'http://127.0.0.1:7400',
+ },
+ },
+ {
+ // 2026-05-28 wire-up — weekly_intelligence was never running on prod
+ // (72d stale per yolo task 49). compute-intelligence.ts is the
+ // documented "nightly intelligence cron script" but had no scheduler.
+ // tsx is required (TypeScript source). Per-active-org Gemini cost.
+ name: 'norma-intelligence',
+ script: 'scripts/compute-intelligence.ts',
+ cwd: '/root/Projects/Norma',
+ interpreter: 'npx',
+ interpreter_args: 'tsx --env-file=/root/Projects/Norma/.env.local',
+ // 08:00 UTC = midnight PT — "nightly" per the script header
+ cron_restart: '0 8 * * *',
+ autorestart: false,
+ watch: false,
+ env: {
+ NODE_ENV: 'production',
+ },
+ },
],
};
diff --git a/scripts/daily-cron.js b/scripts/daily-cron.js
new file mode 100755
index 0000000..dd9f9c6
--- /dev/null
+++ b/scripts/daily-cron.js
@@ -0,0 +1,57 @@
+#!/usr/bin/env node
+/**
+ * Daily "Morning Batch" Cron Script
+ *
+ * Triggered by PM2 cron_restart at 14:00 UTC (= 6:00 AM PT) — matches the
+ * /api/cron/daily route header's documented cadence.
+ *
+ * POSTs to the Next.js app's /api/cron/daily endpoint with the x-cron-secret
+ * header, which is the preferred auth path per lib/cron-auth.ts (method 1).
+ * Endpoint calls generateDailyDrafts('cron') which inserts a new row into
+ * sessions (title = "Morning Batch — <date>") plus 3 lane drafts.
+ *
+ * Mirrors the credential-health-cron.js pattern. Loaded via --env-file so
+ * CRON_SECRET + NORMA_URL come from .env.local (see ecosystem.config.js).
+ */
+
+const NORMA_URL = process.env.NORMA_URL || 'http://127.0.0.1:7400';
+const CRON_SECRET = process.env.CRON_SECRET || process.env.AUTH_PASSWORD || '';
+
+async function run() {
+ const started = Date.now();
+ console.log(`[daily-cron] Starting at ${new Date().toISOString()}`);
+
+ try {
+ const resp = await fetch(`${NORMA_URL}/api/cron/daily`, {
+ method: 'POST',
+ headers: {
+ 'x-cron-secret': CRON_SECRET,
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ if (!resp.ok) {
+ const body = await resp.text();
+ console.error(`[daily-cron] HTTP ${resp.status}: ${body}`);
+ process.exit(1);
+ }
+
+ const data = await resp.json();
+ const elapsed = ((Date.now() - started) / 1000).toFixed(1);
+
+ console.log(`[daily-cron] Done in ${elapsed}s — sessionId=${data.sessionId} drafts=${data.draftsCreated}`);
+ if (data.drafts) {
+ for (const d of data.drafts) {
+ console.log(` · lane ${d.lane}: ${d.subject}`);
+ }
+ }
+ if (data.errors && data.errors.length) {
+ console.log(` errors: ${data.errors.join('; ')}`);
+ }
+ } catch (e) {
+ console.error(`[daily-cron] EXCEPTION: ${e.message}`);
+ process.exit(1);
+ }
+}
+
+run();
← 0713748 norma-scraper: load .env.local via interpreter_args so SESSI
·
back to Norma
·
security: strip hardcoded dw_admin DSN password -> env-first 1033d7f →