[object Object]

← back to Beverlyhillsvideos

BHV watcher: fail-loud (heartbeat latest.json + CNCP alert on unwired auth) — closes Cody's silent-failure gap

c46166852a82b3fce34bdaf9e1bb8117dd2f9ee1 · 2026-08-07 06:46:14 -0700 · Steve Abrams

Files touched

Diff

commit c46166852a82b3fce34bdaf9e1bb8117dd2f9ee1
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Fri Aug 7 06:46:14 2026 -0700

    BHV watcher: fail-loud (heartbeat latest.json + CNCP alert on unwired auth) — closes Cody's silent-failure gap
---
 .gitignore                         |  1 +
 scripts/adsense-approval-watch.mjs | 37 ++++++++++++++++++++++++++++++++++---
 2 files changed, 35 insertions(+), 3 deletions(-)

diff --git a/.gitignore b/.gitignore
index 0260354..2b42ade 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,3 +15,4 @@ filmgen/work/
 # BHV AdSense watcher runtime (TK-10317)
 data/adsense-watch-state.json
 data/adsense-watch.log
+data/latest.json
diff --git a/scripts/adsense-approval-watch.mjs b/scripts/adsense-approval-watch.mjs
index 274d0cf..5a2150b 100644
--- a/scripts/adsense-approval-watch.mjs
+++ b/scripts/adsense-approval-watch.mjs
@@ -117,9 +117,39 @@ function alertHtml(outcome, m) {
   </div>`;
 }
 
+// Heartbeat for dw-canary-meta-watchdog (reads data/latest.json mtime) + a status record
+// so a broken run is VISIBLE, not silent (Cody's cycle-1 gap). Best-effort; never throws.
+function writeHeartbeat(status, detail) {
+  try {
+    mkdirSync(dirname(STATE_FILE), { recursive: true });
+    writeFileSync(resolve(PROJ, 'data/latest.json'),
+      JSON.stringify({ ts: new Date().toISOString(), site: SITE, status, detail: detail || '' }, null, 2));
+  } catch { /* */ }
+}
+
+// Fail-loud on a broken run: post ONE CNCP parking-lot card (deduped via state), so a
+// permanently-unwired watcher surfaces even though it can't email (no George auth). Local, best-effort.
+async function cncpAlertOnce(state, key, title, body) {
+  if (state.cncpAlerted?.includes(key)) return;
+  try {
+    const res = await fetch('http://127.0.0.1:3333/api/parking-lot', {
+      method: 'POST', headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify({ project: 'beverlyhillsvideos', title, note: body, source: 'bhv-adsense-approval-watch' }),
+    });
+    if (res.ok) { (state.cncpAlerted ||= []).push(key); saveState(state); }
+  } catch { /* CNCP down — heartbeat already recorded the failure */ }
+}
+
 async function main() {
   const auth = loadAuth();
-  if (!auth) { log('FATAL: no George auth (set GEORGE_BASIC_AUTH or add keychain item: security add-generic-password -s dw-agents -a admin -w "<pw>")'); process.exit(2); }
+  if (!auth) {
+    log('FATAL: no George auth (set GEORGE_BASIC_AUTH or add keychain item: security add-generic-password -s dw-agents -a admin -w "<pw>")');
+    writeHeartbeat('AUTH_UNWIRED', 'George keychain/env auth missing — watcher cannot run until wired');
+    await cncpAlertOnce(loadState(), 'auth_unwired',
+      'BHV AdSense watcher is UNWIRED (silent)',
+      'scripts/adsense-approval-watch.mjs exits 2 — no George auth in keychain/env. It will NEVER email the approval until Steve wires it (TK-10317). Fix: security add-generic-password -s dw-agents -a admin -w "<pw>".');
+    process.exit(2);
+  }
 
   if (SELF_TEST) {
     const r = await george('/api/search', { auth, query: { account: ACCOUNT, query: `from:${ADSENSE_SENDER} newer_than:2y`, maxResults: 3 } });
@@ -140,7 +170,7 @@ async function main() {
   // pass the search but be wrongly dropped here (a silent false-negative — the worst
   // failure for a "tell me when it's live" watcher). Dedup on message-id only.
   const fresh = msgs.filter((m) => m.id && !state.alerted.includes(m.id));
-  if (!fresh.length) { log('no new decision — staying quiet'); return; }
+  if (!fresh.length) { log('no new decision — staying quiet'); writeHeartbeat('OK', `no decision yet (${msgs.length} msgs scanned)`); return; }
 
   for (const m of fresh) {
     const outcome = classify(`${m.subject || ''} ${m.snippet || ''}`);
@@ -158,6 +188,7 @@ async function main() {
     state.alerted.push(m.id);
   }
   if (!DRY) saveState(state);
+  writeHeartbeat('DECISION_SENT', `${fresh.length} decision(s) alerted`);
 }
 
-main().catch((e) => { log(`ERROR: ${e.message}`); process.exit(1); });
+main().catch((e) => { log(`ERROR: ${e.message}`); writeHeartbeat('ERROR', e.message); process.exit(1); });

← a8bab17 auto-data-snapshot: 2026-08-07T06:27:13 (1 data files) — soc  ·  back to Beverlyhillsvideos  ·  poster: harden for unattended ops (Cody terminal-gate FIX-FI 49e3fa0 →