[object Object]

← back to Cronjobs Viewer

separate launchd alerts from job errors

5316d029502034eeaef43d982d2def5a3929cda0 · 2026-08-30 11:05:31 -0700 · Steve

Files touched

Diff

commit 5316d029502034eeaef43d982d2def5a3929cda0
Author: Steve <steve@designerwallcoverings.com>
Date:   Sun Aug 30 11:05:31 2026 -0700

    separate launchd alerts from job errors
---
 public/index.html | 17 ++++++++++-------
 server.js         | 21 ++++++++++++++++++---
 2 files changed, 28 insertions(+), 10 deletions(-)

diff --git a/public/index.html b/public/index.html
index 0e89ffb..077b930 100644
--- a/public/index.html
+++ b/public/index.html
@@ -46,7 +46,7 @@
   .stats{display:flex;gap:14px;flex-wrap:wrap;margin-top:10px}
   .stat{background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:6px 12px;font-size:12px}
   .stat b{font-size:16px;display:block}
-  .stat.err b{color:var(--red)} .stat.run b{color:var(--blue)} .stat.ok b{color:var(--green)}
+  .stat.err b{color:var(--red)} .stat.alert b{color:var(--amber)} .stat.run b{color:var(--blue)} .stat.ok b{color:var(--green)}
   .controls{display:flex;gap:12px;flex-wrap:wrap;align-items:center;padding:12px 20px;border-bottom:1px solid var(--line);background:var(--panel)}
   .controls label{font-size:11px;color:var(--dim);text-transform:uppercase;letter-spacing:.5px;display:flex;flex-direction:column;gap:4px}
   select,input[type=search]{background:var(--panel2);color:var(--ink);border:1px solid var(--line);border-radius:6px;padding:6px 9px;font-size:13px}
@@ -58,11 +58,12 @@
   .grid{display:grid;grid-template-columns:repeat(var(--cols),minmax(0,1fr));gap:12px}
   .card{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:12px 13px;display:flex;flex-direction:column;gap:7px;position:relative;overflow:hidden}
   .card.error{border-color:#5b2626;background:linear-gradient(180deg,#1b1113,#14181d)}
+  .card.alert{border-color:#6a4b16;background:linear-gradient(180deg,#211a0d,#14181d)}
   .card.running{border-color:#22406b}
   .card .label{font-weight:600;font-size:13px;word-break:break-all;padding-right:70px}
   .card .short{color:var(--dim);font-weight:400}
   .badge{position:absolute;top:11px;right:12px;font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.6px;padding:3px 7px;border-radius:20px}
-  .badge.error{background:#5b2626;color:#fecaca} .badge.running{background:#22406b;color:#bfdbfe}
+  .badge.error{background:#5b2626;color:#fecaca} .badge.alert{background:#6a4b16;color:#fde68a} .badge.running{background:#22406b;color:#bfdbfe}
   .badge.idle,.badge.active{background:#20272f;color:#9fb0c0}
   /* liveness pill (true launchd run-accounting signal, not log mtime) */
   .live{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;padding:2px 7px;border-radius:20px;display:inline-block}
@@ -113,6 +114,7 @@
     <select id="fstate">
       <option value="">All states</option>
       <option value="error">Error</option>
+      <option value="alert">Alert</option>
       <option value="running">Running</option>
       <option value="active">Active</option>
     </select>
@@ -193,7 +195,7 @@ function apply(){
   const q = $('#q').value.trim().toLowerCase();
   const fstate = $('#fstate').value, fcad=$('#fcad').value, flive=$('#flive').value, failonly=$('#failonly').checked;
   let rows = jobs.filter(j=>{
-    if(failonly && j.state!=='error') return false;
+    if(failonly && j.state!=='error' && j.state!=='alert') return false;
     if(fstate && j.state!==fstate) return false;
     if(flive && (j.liveness||'unknown')!==flive) return false;
     if(fcad){ const s=(j.schedule||''); if(fcad==='daily'&&!s.startsWith('daily'))return false; if(fcad==='every'&&!s.startsWith('every'))return false; if(fcad==='at load'&&s!=='at load')return false; }
@@ -201,7 +203,7 @@ function apply(){
     return true;
   });
   const sort = $('#sort').value;
-  const stateRank={error:0,running:1,active:2,idle:2};
+  const stateRank={error:0,alert:1,running:2,active:3,idle:3};
   const liveRank={stale:0,unknown:1,firing:2};
   rows.sort((a,b)=>{
     switch(sort){
@@ -228,14 +230,14 @@ function apply(){
     // liveness label: firing shows it IS running even if the log mtime is frozen
     const liveLabel = live==='firing'?'🟢 firing':live==='stale'?'⚠ stale':'? unknown';
     return `<div class="card ${j.state} ${live==='stale'?'stale':''}" data-label="${j.label}">
-      ${drill('state',j.state,`<span class="badge ${j.state}">${j.state}</span>`,'')}
+      ${drill('state',j.state,j.state,`badge ${j.state}`)}
       <div class="label">com.steve.<span class="short">${short}</span></div>
       <div><span class="live ${live}" role="button" tabindex="0" data-label="${j.label}" title="click to re-check this job's live launchd run-accounting now (bypasses the 5-min cache)">${liveLabel} <span class="reck">↻</span></span></div>
       ${j.purpose?`<div class="purpose">${escapeHtml(j.purpose)}</div>`:''}
       <div class="rowset">
         <div class="kv"><span class="k">Schedule</span><span class="v"><span class="chip">${j.schedule||'—'}</span></span></div>
         <div class="kv"><span class="k">Runs</span><span class="v runsv">${runsTxt}</span></div>
-        <div class="kv"><span class="k">Last exit</span><span class="v ${exitClass(ex)}">${ex===null?'not loaded':ex==='-'?'—':ex}${ex==='0'?' ✓':(ex&&ex!=='-'?' ✕':'')}</span></div>
+        <div class="kv"><span class="k">Last exit</span><span class="v ${exitClass(ex)}">${ex===null?'not loaded':ex==='-'?'—':ex}${ex==='0'?' ✓':(ex&&ex!=='-'?(j.state==='alert'?' ⚠':' ✕'):'')}</span></div>
         <div class="kv"><span class="k">Last log</span><span class="v" title="log mtime — frozen for silent jobs, NOT a real-run signal · ${j.lastLogWrite||''}">${fmt(j.lastLogWrite)||'—'} <span class="when">${rel(j.lastLogWrite)}</span></span></div>
         <div class="kv"><span class="k">Next run</span><span class="v" title="${j.nextRun||''}">${fmt(j.nextRun)||'(interval)'} <span class="when">${rel(j.nextRun)}</span></span></div>
         <div class="kv"><span class="k">Program</span><span class="v"><span class="chip">${j.program||'—'}</span></span></div>
@@ -250,12 +252,13 @@ function escapeHtml(s){return String(s).replace(/[&<>"]/g,c=>({'&':'&amp;','<':'
 
 function stats(){
   const j=DATA.jobs||[];
-  const err=j.filter(x=>x.state==='error').length, run=j.filter(x=>x.state==='running').length;
+  const err=j.filter(x=>x.state==='error').length, alert=j.filter(x=>x.state==='alert').length, run=j.filter(x=>x.state==='running').length;
   const firing=j.filter(x=>x.liveness==='firing').length, stale=j.filter(x=>x.liveness==='stale').length;
   $('#stats').innerHTML =
     `<div class="stat"><b>${DATA.count||j.length}</b>total jobs</div>`+
     `<div class="stat"><b>${DATA.loadedCount||0}</b>loaded</div>`+
     `<div class="stat err"><b>${err}</b>error exit</div>`+
+    `<div class="stat alert"><b>${alert}</b>alert exit</div>`+
     `<div class="stat run"><b>${run}</b>running</div>`+
     `<div class="stat ok"><b>${firing}</b>firing</div>`+
     `<div class="stat err"><b>${stale}</b>stale</div>`;
diff --git a/server.js b/server.js
index 91af89c..2f584fb 100644
--- a/server.js
+++ b/server.js
@@ -5,7 +5,7 @@
  * Introspects EVERY com.steve.* LaunchAgent live (launchctl list + the plist
  * files in ~/Library/LaunchAgents) and serves a single full-page viewer that
  * shows, per job: label + inferred purpose, schedule, last run, next run, last
- * exit code, state (idle/running/error), program, and model.
+ * exit code, state (active/running/alert/error), program, and model.
  *
  * The launchd-introspection core (readCronPlistMeta / collectCrons / sh) is
  * lifted verbatim from the AbramsEgo command center (:9773) /api/crons handler
@@ -33,6 +33,15 @@ const AUTH_PASS = process.env.CRONJOBS_PASS || 'DW2024!';
 const SNAPSHOT_PATH = path.join(__dirname, 'data', 'liveness-snapshot.json');
 const RUNS_REFRESH_MS = 5 * 60 * 1000;
 const RUNS_CONCURRENCY = 8;
+// These jobs intentionally encode a detected condition as a non-zero exit.
+// Explicit membership avoids hiding genuine crashes behind a name heuristic.
+const ALERT_EXIT_LABELS = new Set([
+  'com.steve.dw-backup-canary',
+  'com.steve.gmc-feed-guard',
+  'com.steve.pm2-fracture-canary',
+  'com.steve.chargeandexplore-release-canary',
+  'com.steve.enrichment-health',
+]);
 
 // ---- launchd introspection (lifted from AbramsEgo server.js) ---------------
 function sh(cmd, args, timeoutMs = 8000) {
@@ -140,9 +149,15 @@ async function collectCrons() {
     const pid = l && l.pid !== '-' ? l.pid : null;
     const lastExit = l ? l.exit : null;
     const meta = readCronPlistMeta(path.join(laDir, f));
+    // Some canaries deliberately use a non-zero exit to mean "condition found".
+    // Keep that operational alert distinct from a job that crashed. This list is
+    // explicit: broad canary/guard name matching would hide genuine script errors.
+    const nonzeroExit = lastExit && lastExit !== '0' && lastExit !== '-';
     // State from launchd TRUTH, not log mtime: running if a pid is present,
-    // error if the last exit was a non-zero/non-dash code, else active.
-    const state = pid ? 'running' : (lastExit && lastExit !== '0' && lastExit !== '-') ? 'error' : 'active';
+    // alert for a known signal-style nonzero exit, error for other nonzero exits.
+    const state = pid ? 'running' : nonzeroExit
+      ? (ALERT_EXIT_LABELS.has(label) ? 'alert' : 'error')
+      : 'active';
     const live = livenessFor(RUNS_SNAPSHOT[label]);
     // Purpose: prefer the plist's own comment; else de-slug the label into a
     // readable phrase (com.steve.dw-shopify-storage-canary -> "dw shopify

← dc5aa61 add cross-browser cron viewer proof  ·  back to Cronjobs Viewer  ·  (newest)