← back to Builds Nightly

lib/wins-for-day.mjs

87 lines

#!/usr/bin/env node
// Pull CNCP wins for ONE calendar day (local time) and shape them into
// { date, dateLabel, count, bullets[], narration } for the nightly video.
// Usage: node lib/wins-for-day.mjs 2026-07-22   → prints JSON to stdout.
import http from 'node:http';

const CNCP = process.env.CNCP_URL || 'http://127.0.0.1:3333/api/wins';
const day = process.argv[2];
if (!/^\d{4}-\d{2}-\d{2}$/.test(day || '')) {
  console.error('usage: wins-for-day.mjs YYYY-MM-DD');
  process.exit(2);
}

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

// A win's timestamp, best-effort across the field names CNCP has used.
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;
}
// Local-time calendar-day key (YYYY-MM-DD) for a Date.
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();

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 || []);

const dayWins = [];
for (const w of wins) {
  // CNCP stamps a clean 'date' string (YYYY-MM-DD) — use it verbatim (no TZ math).
  // Fall back to parsing a timestamp only when 'date' is absent.
  const dstr = typeof w.date === 'string' && /^\d{4}-\d{2}-\d{2}/.test(w.date) ? w.date.slice(0, 10) : null;
  if (dstr) { if (dstr === day) dayWins.push(w); continue; }
  const t = winTime(w);
  if (t && dayKey(t) === day) dayWins.push(w);
}

// Dedupe by title; keep first-seen order.
const seen = new Set();
const uniq = [];
for (const w of dayWins) {
  const title = clean(w.title || w.name || w.summary);
  if (!title || seen.has(title.toLowerCase())) continue;
  seen.add(title.toLowerCase());
  uniq.push({ title, project: clean(w.project || w.area || '') });
}

const dt = new Date(day + 'T12:00:00');
const dateLabel = dt.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' });
const shortLabel = dt.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });

// On-screen: up to 8 bullets. Narration: a flowing recap, capped ~780 chars
// (ElevenLabs turbo does ~15 chars/sec → keeps clips ~45-70s).
const bullets = uniq.slice(0, 8);
let narration = '';
if (uniq.length === 0) {
  narration = '';
} else {
  const n = uniq.length;
  const lead = `${dateLabel}. ${n === 1 ? 'One build landed' : `${n} builds landed`} today.`;
  const parts = [lead];
  for (const w of uniq) {
    if (parts.join(' ').length > 720) break;
    parts.push(w.project ? `${w.title} on ${w.project}.` : `${w.title}.`);
  }
  narration = parts.join(' ').slice(0, 800);
}

console.log(JSON.stringify({
  date: day, dateLabel, shortLabel,
  count: uniq.length, bullets, narration,
}, null, 1));