← 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
M scripts/canary-freshness.js
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 →