← 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));