← 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
M build-day.shA lib/build-transcripts.mjs
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)