← back to Builds Nightly
lib/build-transcripts.mjs
79 lines
#!/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));