[object Object]

← back to Ventura Corridor

iter 69: BTRC data freshness pill on /today.html — /api/data-freshness reads MAX(first_seen_at) for source LIKE '%btrc%' rows, computes days_since_ingest, classifies status as fresh (≤7d) / stale (≤30d) / very_stale / never; pill in /today.html header shows colored ✓/⚠/✕ icon + days-since label + BTRC row count, hover reveals last_btrc_ingest timestamp + source URL (data.lacity.org/6rrh-rzua) + recommended npm command (ingest:la_btrc); auto-refresh every 5 min; currently shows '✓ today · 17,238 rows' in green (last ingest 2026-05-06 15:14 UTC, fresh)

140c1a6906ae116acc8a05009782c2522656ebf4 · 2026-05-06 15:30:18 -0700 · SteveStudio2

Files touched

Diff

commit 140c1a6906ae116acc8a05009782c2522656ebf4
Author: SteveStudio2 <steve@designerwallcoverings.com>
Date:   Wed May 6 15:30:18 2026 -0700

    iter 69: BTRC data freshness pill on /today.html — /api/data-freshness reads MAX(first_seen_at) for source LIKE '%btrc%' rows, computes days_since_ingest, classifies status as fresh (≤7d) / stale (≤30d) / very_stale / never; pill in /today.html header shows colored ✓/⚠/✕ icon + days-since label + BTRC row count, hover reveals last_btrc_ingest timestamp + source URL (data.lacity.org/6rrh-rzua) + recommended npm command (ingest:la_btrc); auto-refresh every 5 min; currently shows '✓ today · 17,238 rows' in green (last ingest 2026-05-06 15:14 UTC, fresh)
---
 public/today.html   | 21 ++++++++++++++++++++-
 src/server/index.ts | 35 +++++++++++++++++++++++++++++++++++
 2 files changed, 55 insertions(+), 1 deletion(-)

diff --git a/public/today.html b/public/today.html
index 689bdda..5229572 100644
--- a/public/today.html
+++ b/public/today.html
@@ -105,7 +105,7 @@
 <header>
   <div>
     <h1>Today · <em>DW pipeline</em></h1>
-    <div class="sub">your one-page action plan</div>
+    <div class="sub">your one-page action plan <span id="freshness-pill" style="margin-left:14px;font-size:9px;letter-spacing:.18em;padding:2px 8px;border:1px solid var(--rule);color:var(--ink-mute)" title="Click for ingest details">data: —</span></div>
   </div>
   <nav class="tabs">
     <a href="/">map</a>
@@ -517,6 +517,25 @@ async function loadPrioStrip() {
   }
 }
 loadPrioStrip();
+
+// ─── Data freshness pill (iter 69) ──────────────────────────────────
+async function loadFreshness() {
+  try {
+    const d = await fetch('/api/data-freshness').then(r => r.json());
+    const pill = document.getElementById('freshness-pill');
+    if (!pill) return;
+    const cls = ({ fresh: 'green', stale: 'metal', very_stale: 'red', never: 'red' })[d.status] || 'mute';
+    const color = ({ fresh: 'var(--green,#6a9b73)', stale: 'var(--metal)', very_stale: 'var(--red,#b66565)', never: 'var(--red,#b66565)' })[d.status];
+    const icon  = ({ fresh: '✓', stale: '⚠', very_stale: '⚠', never: '✕' })[d.status];
+    const days  = d.days_since_ingest;
+    pill.innerHTML = `data: <b style="color:${color};font-family:var(--mono);font-size:11px">${icon} ${days === null ? '—' : (days === 0 ? 'today' : days === 1 ? '1 day' : days + ' days')}</b> · ${(d.btrc_total || 0).toLocaleString()} BTRC rows`;
+    pill.style.borderColor = color || 'var(--rule)';
+    pill.style.color = color || 'var(--ink-mute)';
+    pill.title = `Last BTRC ingest: ${d.last_btrc_ingest || 'never'}\nSource: ${d.source_url}\nRefresh command: ${d.ingest_command}`;
+  } catch {}
+}
+loadFreshness();
+setInterval(loadFreshness, 5 * 60_000);
 </script>
 </body>
 </html>
diff --git a/src/server/index.ts b/src/server/index.ts
index b6b4b9b..ab47feb 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -68,6 +68,7 @@ const ADMIN_PATHS = [
   /^\/api\/followups\/?$/i,
   /^\/api\/snapshots\/?$/i,
   /^\/api\/activity\/?$/i,
+  /^\/api\/data-freshness\/?$/i,
   /^\/crawl-derby(\.html)?\/?$/i,
   /^\/api\/crawl(\/.*)?$/i,
   /^\/postcards(\.html)?\/?$/i,
@@ -1704,6 +1705,40 @@ app.get('/api/responses.csv', async (_req, res) => {
   }
 });
 
+// ─── Data-freshness signal: when was the last BTRC ingest, are we stale? ──
+app.get('/api/data-freshness', async (_req, res) => {
+  try {
+    const r = await query(`
+      SELECT
+        count(*) FILTER (WHERE source LIKE '%btrc%' OR source = 'la_btrc')                AS btrc_total,
+        MAX(first_seen_at) FILTER (WHERE source LIKE '%btrc%' OR source = 'la_btrc')      AS last_btrc_ingest,
+        count(*)                                                                          AS all_total,
+        MAX(last_seen_at)                                                                 AS last_any_seen
+      FROM businesses
+    `);
+    const row = r.rows[0];
+    const lastIngest = row.last_btrc_ingest ? new Date(row.last_btrc_ingest) : null;
+    const daysSince = lastIngest ? Math.floor((Date.now() - lastIngest.getTime()) / 86_400_000) : null;
+    let status: 'fresh' | 'stale' | 'very_stale' | 'never' = 'never';
+    if (daysSince !== null) {
+      if (daysSince <= 7)       status = 'fresh';
+      else if (daysSince <= 30) status = 'stale';
+      else                      status = 'very_stale';
+    }
+    res.json({
+      btrc_total: Number(row.btrc_total),
+      last_btrc_ingest: row.last_btrc_ingest,
+      days_since_ingest: daysSince,
+      status,
+      all_total: Number(row.all_total),
+      source_url: 'https://data.lacity.org/resource/6rrh-rzua.json',
+      ingest_command: 'npm run ingest:la_btrc'
+    });
+  } catch (e: any) {
+    res.status(500).json({ error: e.message });
+  }
+});
+
 // ─── Per-vertical conversion: which pitch_type replies/wins most ──────
 app.get('/api/responses/by-vertical', async (_req, res) => {
   try {

← 43adaa8 iter 68: per-vertical conversion stats — /api/responses/by-v  ·  back to Ventura Corridor  ·  iter 70: /pitches.html bulk-set-followup — extends iter 66 m d0c4c91 →