← back to Cronjobs Viewer
separate launchd alerts from job errors
5316d029502034eeaef43d982d2def5a3929cda0 · 2026-08-30 11:05:31 -0700 · Steve
Files touched
M public/index.htmlM server.js
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=>({'&':'&','<':'
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)