[object Object]

← 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

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 →