[object Object]

← back to Marketing Command Center

Add durable regenerate+publish job for calendarhub activation snapshot

cd7a2c537ebf46a80358bba363f1ea1995a827fa · 2026-08-25 12:40:12 -0700 · Steve Abrams

Timeline activation layer froze at 2026-08-16 (~a week) because prod (Kamatera)
can't project bulk_fivefield_worklist (Mac2-only, split-ownership) and nothing
regenerated+redeployed the snapshot file. This Mac2 job regenerates from the live
worklist, HARD-validates (activation_live + count floor + window anchored to
today+1), atomically publishes the single file to Kamatera, and busts prod cache.
launchd plist staged but NOT installed (scheduled-job install is gated).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit cd7a2c537ebf46a80358bba363f1ea1995a827fa
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue Aug 25 12:40:12 2026 -0700

    Add durable regenerate+publish job for calendarhub activation snapshot
    
    Timeline activation layer froze at 2026-08-16 (~a week) because prod (Kamatera)
    can't project bulk_fivefield_worklist (Mac2-only, split-ownership) and nothing
    regenerated+redeployed the snapshot file. This Mac2 job regenerates from the live
    worklist, HARD-validates (activation_live + count floor + window anchored to
    today+1), atomically publishes the single file to Kamatera, and busts prod cache.
    launchd plist staged but NOT installed (scheduled-job install is gated).
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 deploy/com.steve.marketing-calendar-snapshot.plist |  52 ++++++++++
 scripts/publish-calendar-snapshot.mjs              | 114 +++++++++++++++++++++
 2 files changed, 166 insertions(+)

diff --git a/deploy/com.steve.marketing-calendar-snapshot.plist b/deploy/com.steve.marketing-calendar-snapshot.plist
new file mode 100644
index 0000000..d5197c4
--- /dev/null
+++ b/deploy/com.steve.marketing-calendar-snapshot.plist
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Daily regenerate + atomic-publish of the /#calendarhub activation snapshot to
+     Kamatera, so the Timeline never re-freezes. NOT AUTO-INSTALLED — installing a
+     scheduled job is gated. Install with:
+       launchctl bootstrap gui/$(id -u) ~/Projects/marketing-command-center/deploy/com.steve.marketing-calendar-snapshot.plist
+     Undo with:
+       launchctl bootout gui/$(id -u)/com.steve.marketing-calendar-snapshot
+     PGUSER is pinned because a headless launchd run has no $USER, and the pg pool
+     falls back to $USER (else 'stevestudio2', a wrong user) for the /tmp socket. -->
+<plist version="1.0">
+<dict>
+    <key>Label</key>
+    <string>com.steve.marketing-calendar-snapshot</string>
+
+    <key>ProgramArguments</key>
+    <array>
+        <string>/opt/homebrew/bin/node</string>
+        <string>/Users/macstudio3/Projects/marketing-command-center/scripts/publish-calendar-snapshot.mjs</string>
+    </array>
+
+    <key>WorkingDirectory</key>
+    <string>/Users/macstudio3/Projects/marketing-command-center</string>
+
+    <key>EnvironmentVariables</key>
+    <dict>
+        <key>PATH</key>
+        <string>/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
+        <key>PGHOST</key>
+        <string>/tmp</string>
+        <key>PGDATABASE</key>
+        <string>dw_unified</string>
+        <key>PGUSER</key>
+        <string>macstudio3</string>
+    </dict>
+
+    <key>StartCalendarInterval</key>
+    <dict>
+        <key>Hour</key>
+        <integer>6</integer>
+        <key>Minute</key>
+        <integer>30</integer>
+    </dict>
+
+    <key>RunAtLoad</key>
+    <false/>
+
+    <key>StandardOutPath</key>
+    <string>/Users/macstudio3/Projects/marketing-command-center/data/publish-calendar-snapshot.log</string>
+    <key>StandardErrorPath</key>
+    <string>/Users/macstudio3/Projects/marketing-command-center/data/publish-calendar-snapshot.log</string>
+</dict>
+</plist>
diff --git a/scripts/publish-calendar-snapshot.mjs b/scripts/publish-calendar-snapshot.mjs
new file mode 100644
index 0000000..9995312
--- /dev/null
+++ b/scripts/publish-calendar-snapshot.mjs
@@ -0,0 +1,114 @@
+#!/usr/bin/env node
+// publish-calendar-snapshot.mjs — the DURABLE fix for the /#calendarhub Timeline
+// staleness bug (activation layer froze at 2026-08-16 for ~a week).
+//
+// WHY THIS EXISTS: the Timeline "activation" layer projects from the Postgres
+// table bulk_fivefield_worklist, which is Mac2-ONLY (split-ownership — staging
+// tables are Mac2-canonical and deliberately NOT on Kamatera). So prod can never
+// project it live; it serves the activation layer from the last-deployed snapshot
+// file data/calendars-activation.json. Nothing regenerated + redeployed that file,
+// so it froze. This script closes that pipeline gap: regenerate on Mac2 (where the
+// worklist lives) -> HARD-VALIDATE -> atomically publish the single file to Kamatera
+// -> bust prod's 10-min mem cache. It NEVER syncs the worklist table itself
+// (that would violate split-ownership) — it publishes the projected snapshot only.
+//
+// Safe by design: read-only projection + single-file atomic publish. On ANY
+// validation failure it ABORTS the publish (prod keeps its last good snapshot),
+// posts a CNCP parking-lot card (best-effort), and exits nonzero so the existing
+// launchd-job-canary flags it. Intended to run daily via launchd
+// (com.steve.marketing-calendar-snapshot).
+
+import { createRequire } from 'node:module';
+import { execFileSync } from 'node:child_process';
+import { fileURLToPath } from 'node:url';
+import path from 'node:path';
+
+const require = createRequire(import.meta.url);
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+const ROOT = path.join(__dirname, '..');
+const SNAPSHOT = path.join(ROOT, 'data', 'calendars-activation.json');
+
+// ── config (abs tool paths — this runs headless under launchd, no shell PATH) ──
+const RSYNC = '/opt/homebrew/bin/rsync';
+const SSH = '/usr/bin/ssh';
+const CURL = '/usr/bin/curl';
+const REMOTE_HOST = 'root@45.61.58.125';
+const REMOTE_DIR = '/root/DW-Agents/marketing-command-center/data';
+const REMOTE_FINAL = `${REMOTE_DIR}/calendars-activation.json`;
+const REMOTE_TMP = `${REMOTE_DIR}/.calendars-activation.json.tmp`;
+const PUBLIC_URL = 'https://marketing.designerwallcoverings.com/api/calendars/events?fresh=1';
+const AUTH = `${process.env.MCC_HTTP_USER || 'admin'}:${process.env.MCC_HTTP_PASS || 'DW2024!'}`;
+const MIN_ITEMS = parseInt(process.env.SNAP_MIN_ITEMS, 10) || 15000;
+const CNCP_URL = process.env.CNCP_URL || 'http://127.0.0.1:3333/api/parking-lot';
+
+const log = (m) => console.log(`[publish-calendar-snapshot] ${new Date().toISOString()} ${m}`);
+
+// tomorrow in America/Los_Angeles as YYYY-MM-DD (the activation layer anchors to
+// today+1 in server-local time; Mac2 is PT, but pin the TZ so a TZ drift can't
+// silently pass validation).
+function tomorrowPT() {
+  const now = new Date();
+  const pt = new Date(now.toLocaleString('en-US', { timeZone: 'America/Los_Angeles' }));
+  pt.setDate(pt.getDate() + 1);
+  const y = pt.getFullYear(), m = String(pt.getMonth() + 1).padStart(2, '0'), d = String(pt.getDate()).padStart(2, '0');
+  return `${y}-${m}-${d}`;
+}
+
+function alertCNCP(title, detail) {
+  try {
+    execFileSync(CURL, ['-s', '--max-time', '8', '-X', 'POST', CNCP_URL,
+      '-H', 'Content-Type: application/json',
+      '-d', JSON.stringify({ title, detail, source: 'publish-calendar-snapshot', severity: 'warn' })],
+      { stdio: 'ignore' });
+  } catch { /* best-effort — never let the alert channel sink the run */ }
+}
+
+function fail(reason) {
+  log(`ABORT — ${reason}`);
+  alertCNCP('Calendar snapshot publish FAILED', reason);
+  // clear failure signature for the launchd-job-canary stderr sweep:
+  console.error(`publish-calendar-snapshot: FAIL: ${reason}`);
+  process.exit(1);
+}
+
+async function main() {
+  // 1. regenerate from the LIVE worklist (this writes data/calendars-activation.json)
+  log('regenerating snapshot from live worklist…');
+  const lib = require('../modules/calendars/lib');
+  let data;
+  try { data = await lib.buildEvents(true); }
+  catch (e) { fail(`buildEvents threw: ${e.message}`); }
+
+  // 2. HARD validation gate — publish ONLY a fresh, live, sane projection
+  const items = data.items || [];
+  const first = items[0]?.date;
+  const expected = tomorrowPT();
+  if (data.activation_live !== true) fail(`activation_live is ${data.activation_live} (worklist not readable on Mac2 — refusing to publish a snapshot-of-a-snapshot)`);
+  if (data.activation_error) fail(`activation_error present: ${data.activation_error}`);
+  if (items.length < MIN_ITEMS) fail(`item count ${items.length} < floor ${MIN_ITEMS} (worklist looks truncated/empty)`);
+  if (first !== expected) fail(`first activation date ${first} !== tomorrow ${expected} (window not anchored to today+1)`);
+  log(`validation PASS — activation_live=true, items=${items.length}, window ${first} … ${items[items.length - 1]?.date}`);
+
+  // 3. atomic publish (rsync -> remote temp, then ssh mv), retry once on transient failure
+  const publish = () => {
+    execFileSync(RSYNC, ['-az', '--timeout=60', SNAPSHOT, `${REMOTE_HOST}:${REMOTE_TMP}`], { stdio: 'inherit' });
+    execFileSync(SSH, ['-o', 'ConnectTimeout=20', REMOTE_HOST, `mv ${REMOTE_TMP} ${REMOTE_FINAL}`], { stdio: 'inherit' });
+  };
+  try { publish(); }
+  catch (e1) {
+    log(`publish attempt 1 failed (${e1.message}) — retrying once…`);
+    try { publish(); }
+    catch (e2) { fail(`atomic publish failed twice: ${e2.message}`); }
+  }
+  log('published to Kamatera (atomic temp->rename).');
+
+  // 4. bust prod's in-process mem cache so the calendar jumps to current immediately
+  try {
+    execFileSync(CURL, ['-s', '--max-time', '25', '-u', AUTH, '-o', '/dev/null', PUBLIC_URL], { stdio: 'ignore' });
+    log('prod cache busted via ?fresh=1.');
+  } catch (e) { log(`WARN cache-bust curl failed (non-fatal, TTL will expire in ~10min): ${e.message}`); }
+
+  log('DONE — calendar snapshot current on prod.');
+}
+
+main().catch((e) => fail(`unexpected: ${e.stack || e.message}`));

← 2302081 auto-data-snapshot: 2026-08-25T12:06:42 (1 data files) — pub  ·  back to Marketing Command Center  ·  auto-data-snapshot: 2026-08-25T12:44:18 (1 data files) — pub 23aefba →