[object Object]

← back to La Socrata Ingester

yoloforever c1 FIX (Cody gate): canary reads la_ingest_state for per-source staleness+errors

368772993b0e5cb40eec63cb204b990a5e84928f · 2026-08-11 14:10:29 -0700 · steve

Count-only was false comfort (idempotent upserts keep counts flat while a feed
goes stale/errors). Now checks all 21 sources' last_run age + last_status — verdict
FAIL correctly surfaces gis_zoning stuck in HTTP 502. Covers the 5 count-only tables.

Files touched

Diff

commit 368772993b0e5cb40eec63cb204b990a5e84928f
Author: steve <steve@designerwallcoverings.com>
Date:   Tue Aug 11 14:10:29 2026 -0700

    yoloforever c1 FIX (Cody gate): canary reads la_ingest_state for per-source staleness+errors
    
    Count-only was false comfort (idempotent upserts keep counts flat while a feed
    goes stale/errors). Now checks all 21 sources' last_run age + last_status — verdict
    FAIL correctly surfaces gis_zoning stuck in HTTP 502. Covers the 5 count-only tables.
---
 scripts/canary-freshness.js | 26 ++++++++++++++++++++++++--
 1 file changed, 24 insertions(+), 2 deletions(-)

diff --git a/scripts/canary-freshness.js b/scripts/canary-freshness.js
index 5f2d17d..955166f 100644
--- a/scripts/canary-freshness.js
+++ b/scripts/canary-freshness.js
@@ -51,19 +51,41 @@ async function main() {
     baseline[table] = Math.max(base, count);
   }
 
+  // SOURCE staleness from la_ingest_state — the real "when did we last ingest / did it
+  // error" signal. Covers the count-only tables the row check alone can't see stale, and
+  // surfaces any source whose last run errored (Cody c1: count-only = false comfort).
+  const SRC_MAX_AGE = 14; // days; generous until a scheduled refresh job exists
+  const srcRows = (await q(`SELECT source, last_status, EXTRACT(EPOCH FROM (now()-last_run))/86400 age FROM la_ingest_state`)).rows;
+  const sources = srcRows.map(r => {
+    const age = r.age == null ? null : Math.round(Number(r.age) * 10) / 10;
+    const errored = String(r.last_status || '').startsWith('error');
+    const status = errored ? 'ERRORED' : (age != null && age > SRC_MAX_AGE ? 'STALE' : 'OK');
+    return { source: r.source, ageDays: age, last_status: r.last_status, status };
+  }).sort((a, b) => (b.ageDays || 0) - (a.ageDays || 0));
+
   fs.mkdirSync(path.dirname(BASE), { recursive: true });
   fs.writeFileSync(BASE, JSON.stringify(baseline, null, 2));
   fs.mkdirSync(OUT, { recursive: true });
-  const worst = results.some(r => r.status === 'LOW') ? 'LOW' : results.some(r => r.status === 'STALE') ? 'STALE' : 'OK';
-  fs.writeFileSync(path.join(OUT, 'latest.json'), JSON.stringify({ checked_at: new Date().toISOString?.() || null, verdict: worst, firstRun, results }, null, 2));
+  const anyErr = sources.some(s => s.status === 'ERRORED');
+  const anyStaleSrc = sources.some(s => s.status === 'STALE');
+  const worst = (results.some(r => r.status === 'LOW') || anyErr) ? 'FAIL'
+    : (results.some(r => r.status === 'STALE') || anyStaleSrc) ? 'STALE' : 'OK';
+  fs.writeFileSync(path.join(OUT, 'latest.json'), JSON.stringify({ verdict: worst, firstRun, results, sources }, null, 2));
 
   // report
   console.log(`\nLA data canary — ${firstRun ? 'BASELINE established' : 'verdict ' + worst}\n`);
+  console.log('  ROW COUNTS + freshness:');
   for (const r of results) {
     const mark = r.status === 'OK' ? '✔' : r.status === 'STALE' ? '⏳' : '✖';
     const fresh = r.ageDays != null ? `  fresh ${r.ageDays}d` : '';
     console.log(`  ${mark} ${r.table.padEnd(30)} ${r.count.toLocaleString().padStart(12)}${fresh}${r.reason ? '  — ' + r.reason : ''}`);
   }
+  console.log('\n  SOURCE last-ingest (la_ingest_state):');
+  for (const s of sources) {
+    const mark = s.status === 'OK' ? '✔' : s.status === 'STALE' ? '⏳' : '✖';
+    const age = s.ageDays != null ? `${s.ageDays}d ago` : 'never';
+    console.log(`  ${mark} ${s.source.padEnd(30)} ${age.padStart(10)}${s.status !== 'OK' ? '  — ' + (s.status === 'ERRORED' ? s.last_status : 'stale') : ''}`);
+  }
   console.log('');
 }
 

← 9a4905f yoloforever c1: LA-data freshness+rowcount canary (baseline-  ·  back to La Socrata Ingester  ·  yoloforever c2: per-CD + per-ZIP permit-activity rollups (sc f57ce2c →