[object Object]

← back to Dw Marketing Reels

TK-10395: add read-only publish-status monitor (catches 2207076 recurrence after retry, closes silent-green gap)

d52056c5da78075ff74d2774c8d089f744900b7d · 2026-08-10 08:25:15 -0700 · cre-agent

Files touched

Diff

commit d52056c5da78075ff74d2774c8d089f744900b7d
Author: cre-agent <steve@designerwallcoverings.com>
Date:   Mon Aug 10 08:25:15 2026 -0700

    TK-10395: add read-only publish-status monitor (catches 2207076 recurrence after retry, closes silent-green gap)
---
 scripts/check-publish-status.mjs | 55 ++++++++++++++++++++++++++++++++++++++++
 1 file changed, 55 insertions(+)

diff --git a/scripts/check-publish-status.mjs b/scripts/check-publish-status.mjs
new file mode 100644
index 0000000..c9447e5
--- /dev/null
+++ b/scripts/check-publish-status.mjs
@@ -0,0 +1,55 @@
+#!/usr/bin/env node
+/**
+ * check-publish-status.mjs — TK-10395 safety net.
+ *
+ * The nightly publisher (scripts/publish-social.mjs) deliberately never hard-fails
+ * the cron job (it exits 0 so a bad IG post can't abort the render+push). It records
+ * the real outcome per channel on the newest reel in data/reels.json as
+ *   reels[0].publish.instagram = { status, note, http, ... }
+ *
+ * This READ-ONLY check reads that structured state AFTER a run and exits non-zero when
+ * the Instagram publish did NOT actually land while the pipeline was ARMED — so a
+ * recurrence of the 2207076 container-processing failure (or any publish error) is
+ * caught the morning after instead of silently going green. $0, no network.
+ *
+ * Exit 0 = healthy / not-armed / nothing-to-check.  Exit 1 = armed-but-failed.
+ * Usage: node scripts/check-publish-status.mjs   (optionally READ a pushed copy via REELS_JSON=...)
+ */
+import { readFileSync } from 'node:fs';
+import { join, dirname } from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
+const MAN = process.env.REELS_JSON || join(ROOT, 'data', 'reels.json');
+const ARMED = process.env.SOCIAL_LIVE_ARMED === '1';
+
+function fail(msg) { console.error(`[check-publish] FAIL: ${msg}`); process.exit(1); }
+function ok(msg)   { console.log(`[check-publish] OK: ${msg}`); process.exit(0); }
+
+let reels;
+try { reels = JSON.parse(readFileSync(MAN, 'utf8')); }
+catch (e) { ok(`no readable reels.json (${e.message}) — nothing to check`); }
+
+const list = Array.isArray(reels) ? reels : (reels.reels || reels.items || []);
+if (!list.length) ok('reels.json has no entries — nothing to check');
+
+const reel = list[0]; // publisher prepends newest
+const ig = reel && reel.publish && reel.publish.instagram;
+if (!ig) ok(`newest reel (${reel && reel.file}) has no instagram publish record yet`);
+
+const status = ig.status || 'unknown';
+const detail = `${reel.file} → instagram: ${status}${ig.note ? ' — ' + ig.note : ''}${ig.http ? ' [http ' + ig.http + ']' : ''}`;
+
+// Healthy terminal states (posted, or the deliberate not-yet-live gates).
+const GOOD = ['posted', 'posted-unverified', 'simulated', 'ready-pending-arm', 'held-claims-review'];
+// Gate states that are expected while NOT armed but are a real failure once armed.
+const GATE = ['pending-creds', 'no-creds-for-account', 'ready-pending-arm'];
+
+if (GOOD.includes(status) && status !== 'ready-pending-arm') ok(detail);
+if (!ARMED && (GATE.includes(status) || status === 'error')) ok(`(not armed) ${detail}`);
+
+// Armed + not a healthy post = the TK-10395 failure mode (incl. 2207076 after retry exhaust).
+if (status === 'error' || GATE.includes(status)) fail(`armed IG publish did not land — ${detail}`);
+if (/2207076|attempt 3\/3|Media upload has failed/i.test(ig.note || '')) fail(`transient-retry exhausted — ${detail}`);
+
+ok(detail);

← 50543e2 TK-10395: surface Norma's real IG-publish error instead of a  ·  back to Dw Marketing Reels  ·  TK-10395: fix retry/timeout race (client 90s->240s) + wire p 7ad29bf →