[object Object]

← back to Builds Nightly

searchable transcripts index: build-transcripts.mjs + nightly deploy of transcripts.json

6e2df2c3c379278c6bd27ebe52f866a0ce0fd7c2 · 2026-07-23 08:58:42 -0700 · Steve Abrams

Files touched

Diff

commit 6e2df2c3c379278c6bd27ebe52f866a0ce0fd7c2
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Jul 23 08:58:42 2026 -0700

    searchable transcripts index: build-transcripts.mjs + nightly deploy of transcripts.json
---
 build-day.sh              |  7 +++++
 lib/build-transcripts.mjs | 78 +++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 85 insertions(+)

diff --git a/build-day.sh b/build-day.sh
index 376f0e7..276fa78 100755
--- a/build-day.sh
+++ b/build-day.sh
@@ -93,6 +93,13 @@ m.sort(key=lambda x:x['date'], reverse=True)
 json.dump(m, open(p,'w'), indent=1)
 import sys; print('manifest:', len(m), 'entries', file=sys.stderr)
 PY"
+  # refresh the searchable transcript index (all win titles + projects per film day)
+  if node "$SELF/lib/build-transcripts.mjs" > "$WORK/transcripts.json" 2>/dev/null; then
+    scp -q "$WORK/transcripts.json" "$REMOTE:$REMOTE_DIR/transcripts.json"
+    log "transcripts index refreshed."
+  else
+    log "WARN: transcripts rebuild failed (search index stale, film still deployed)."
+  fi
   log "deployed."
 fi
 echo "OK:$DAY:$COUNT:${SIZE}MB"
diff --git a/lib/build-transcripts.mjs b/lib/build-transcripts.mjs
new file mode 100644
index 0000000..1a56c29
--- /dev/null
+++ b/lib/build-transcripts.mjs
@@ -0,0 +1,78 @@
+#!/usr/bin/env node
+// Build nightly/transcripts.json — the searchable text index for the films.
+// For every day that has a built film (work/<day>/day.json), pull ALL of that
+// day's CNCP wins (not just the 8 on-screen bullets) so the dashboard search
+// can match anything the day shipped, and break out projects (e.g. car wash).
+// Output: [{ date, dateLabel, count, narration, projects[], wins:[{t,p}] }]
+// Usage: node lib/build-transcripts.mjs > transcripts.json
+import http from 'node:http';
+import fs from 'node:fs';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+const ROOT = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
+const CNCP = process.env.CNCP_URL || 'http://127.0.0.1:3333/api/wins';
+
+function getJSON(url) {
+  return new Promise((resolve, reject) => {
+    const req = http.get(url, { timeout: 12000 }, (res) => {
+      let b = '';
+      res.on('data', (c) => (b += c));
+      res.on('end', () => { try { resolve(JSON.parse(b)); } catch (e) { reject(e); } });
+    });
+    req.on('timeout', () => req.destroy(new Error('timeout')));
+    req.on('error', reject);
+  });
+}
+function winTime(w) {
+  for (const k of ['created_at', 'ts', 'date', 'time', 'createdAt', 'at']) {
+    if (w[k]) { const d = new Date(String(w[k])); if (!isNaN(d)) return d; }
+  }
+  return null;
+}
+function dayKey(d) {
+  return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
+}
+const clean = (s) => String(s || '').replace(/\s+/g, ' ').trim();
+
+// Days with a built film = days with work/<day>/day.json
+const days = fs.readdirSync(path.join(ROOT, 'work'))
+  .filter((d) => /^\d{4}-\d{2}-\d{2}$/.test(d) && fs.existsSync(path.join(ROOT, 'work', d, 'day.json')))
+  .sort().reverse();
+
+const data = await getJSON(CNCP).catch((e) => { console.error('CNCP unreachable:', e.message); process.exit(9); });
+const wins = Array.isArray(data) ? data : (data.wins || data.items || []);
+
+// Bucket every win by its clean date string (same rule as wins-for-day.mjs).
+const byDay = new Map();
+for (const w of wins) {
+  const dstr = typeof w.date === 'string' && /^\d{4}-\d{2}-\d{2}/.test(w.date)
+    ? w.date.slice(0, 10)
+    : (() => { const t = winTime(w); return t ? dayKey(t) : null; })();
+  if (!dstr) continue;
+  if (!byDay.has(dstr)) byDay.set(dstr, []);
+  byDay.get(dstr).push(w);
+}
+
+const outArr = [];
+for (const day of days) {
+  const dj = JSON.parse(fs.readFileSync(path.join(ROOT, 'work', day, 'day.json'), 'utf8'));
+  const seen = new Set();
+  const dayWins = [];
+  for (const w of byDay.get(day) || []) {
+    const t = clean(w.title || w.name || w.summary);
+    if (!t || seen.has(t.toLowerCase())) continue;
+    seen.add(t.toLowerCase());
+    dayWins.push({ t, p: clean(w.project || w.area || '') });
+  }
+  outArr.push({
+    date: day,
+    dateLabel: dj.dateLabel,
+    count: dayWins.length || dj.count,
+    narration: dj.narration || '',
+    projects: [...new Set(dayWins.map((w) => w.p).filter(Boolean))].sort(),
+    wins: dayWins,
+  });
+}
+
+console.log(JSON.stringify(outArr, null, 1));

← df39fea fix: manifest print to stderr so backfill/nightly case-match  ·  back to Builds Nightly  ·  (newest)