← back to Ventura Corridor

src/jobs/ig_autopost_dryrun.ts

149 lines

// Dry-run for daily IG autopost. Pulls /api/magazine/today, writes a JSON draft
// + preview HTML. No actual Instagram posting until Steve flips the switch.
//
// Output:
//   ~/Projects/ventura-corridor/tmp/ig-drafts/YYYY-MM-DD.json   ← machine-readable
//   ~/Projects/ventura-corridor/tmp/ig-drafts/YYYY-MM-DD.html   ← visual preview
//   ~/Projects/ventura-corridor/tmp/ig-drafts/index.html        ← all drafts (newest first)
//
// Run manually:
//   pnpm tsx src/jobs/ig_autopost_dryrun.ts
//
// Once Steve approves the format, we wire `instagram-agent` skill to consume
// these drafts at 9 AM each morning.

import * as fs from 'node:fs';
import * as path from 'node:path';

const PORT = process.env.PORT || '9780';
const ADMIN_USER = process.env.ADMIN_USER || 'admin';
const ADMIN_PASS = process.env.ADMIN_PASS || 'DWSecure2024!';
const BASE = `http://127.0.0.1:${PORT}`;
const PUBLIC_BASE = process.env.PUBLIC_BASE || BASE; // when actually posting, swap to public domain
const DRAFTS_DIR = path.resolve(process.cwd(), 'tmp', 'ig-drafts');

function escHtml(s: any): string {
  return String(s ?? '').replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]!));
}

async function main() {
  if (!fs.existsSync(DRAFTS_DIR)) fs.mkdirSync(DRAFTS_DIR, { recursive: true });

  const auth = 'Basic ' + Buffer.from(`${ADMIN_USER}:${ADMIN_PASS}`).toString('base64');
  const r = await fetch(`${BASE}/api/magazine/today`, { headers: { Authorization: auth } });
  if (!r.ok) {
    console.error(`[ig-dryrun] /api/magazine/today returned ${r.status}`);
    process.exit(1);
  }
  const data: any = await r.json();
  if (!data.feature) {
    console.error(`[ig-dryrun] no feature available today`);
    process.exit(0);
  }

  const date = data.date;
  const draft = {
    date,
    feature_id: data.feature.id,
    headline: data.feature.headline,
    biz_name: data.feature.biz_name,
    category: data.feature.category_tag,
    image_url: `${PUBLIC_BASE}${data.share_card}`,
    permalink: `${PUBLIC_BASE}${data.permalink}`,
    audio_url: `${PUBLIC_BASE}${data.audio}`,
    caption: data.caption,
    caption_chars: data.caption_chars,
    tags: data.tags,
    suggested_post_time: `${date}T09:00:00-07:00`,
    status: 'draft',
    generated_at: new Date().toISOString(),
  };

  const jsonPath = path.join(DRAFTS_DIR, `${date}.json`);
  fs.writeFileSync(jsonPath, JSON.stringify(draft, null, 2));

  const htmlPath = path.join(DRAFTS_DIR, `${date}.html`);
  fs.writeFileSync(htmlPath, `<!doctype html>
<html><head><meta charset="utf-8"><title>IG draft · ${escHtml(date)} · The Corridor</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;1,400;1,500&family=Inter:wght@300;400&display=swap');
*{box-sizing:border-box}body{margin:0;background:#0a0a0c;color:#f0ece2;font-family:Inter,sans-serif;font-weight:300;padding:36px}
.wrap{max-width:920px;margin:0 auto;display:grid;grid-template-columns:360px 1fr;gap:36px}
h1{font-family:'Cormorant Garamond',serif;font-style:italic;font-size:42px;margin:0 0 8px}
.kicker{font-size:9px;letter-spacing:.4em;text-transform:uppercase;color:#8a6d3b}
.card iframe{width:360px;height:640px;border:1px solid #2a2622;background:#000}
.meta{font-family:'JetBrains Mono',monospace;font-size:11px;color:#888475;line-height:1.6}
.meta a{color:#b89968;text-decoration:none}
pre{background:rgba(184,153,104,0.06);border:1px solid #2a2622;padding:18px;font-family:'Inter',sans-serif;font-size:14px;line-height:1.5;white-space:pre-wrap;color:#f0ece2}
.tags{margin-top:14px}.tags span{display:inline-block;color:#b89968;font-size:11px;margin-right:8px}
.cta{margin-top:18px;display:flex;gap:8px;flex-wrap:wrap}
.cta a{font-size:9px;letter-spacing:.32em;text-transform:uppercase;color:#b89968;border:1px solid #2a2622;padding:8px 14px;text-decoration:none}
.cta a:hover{border-color:#b89968}
</style></head><body>
<div class="wrap">
  <div class="card">
    <div class="kicker" style="margin-bottom:10px">Share-card preview · 9:16</div>
    <iframe src="${escHtml(data.share_card)}" loading="lazy"></iframe>
  </div>
  <div>
    <div class="kicker">${escHtml(date)} · IG draft</div>
    <h1>${escHtml(data.feature.headline)}</h1>
    <div class="meta">
      ${escHtml(data.feature.biz_name)} · ${escHtml(data.feature.category_tag || '—')}<br>
      <a href="${escHtml(data.permalink)}">${escHtml(data.permalink)}</a><br>
      Suggested post time: ${escHtml(draft.suggested_post_time)}<br>
      Caption: ${data.caption_chars} chars
    </div>
    <h3 style="font-family:'Cormorant Garamond',serif;font-style:italic;color:#b89968;margin:20px 0 6px">Caption</h3>
    <pre>${escHtml(data.caption)}</pre>
    <div class="tags">${(data.tags as string[]).map(t => `<span>${escHtml(t)}</span>`).join('')}</div>
    <div class="cta">
      <a href="${escHtml(data.permalink)}">Read feature</a>
      <a href="${escHtml(data.share_card)}">Share card</a>
      <a href="${escHtml(data.audio)}">Audio</a>
      <a href="index.html">All drafts</a>
    </div>
  </div>
</div>
</body></html>`);

  // index.html — all drafts newest-first
  const drafts = fs.readdirSync(DRAFTS_DIR)
    .filter(n => n.endsWith('.json'))
    .sort()
    .reverse()
    .map(n => JSON.parse(fs.readFileSync(path.join(DRAFTS_DIR, n), 'utf8')));
  fs.writeFileSync(path.join(DRAFTS_DIR, 'index.html'), `<!doctype html>
<html><head><meta charset="utf-8"><title>IG drafts · The Corridor</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;1,400;1,500&family=Inter:wght@300;400&display=swap');
body{margin:0;background:#0a0a0c;color:#f0ece2;font-family:Inter,sans-serif;font-weight:300;padding:32px;max-width:900px;margin:0 auto}
h1{font-family:'Cormorant Garamond',serif;font-style:italic;font-size:42px;margin:0 0 24px;color:#b89968}
.row{display:grid;grid-template-columns:120px 1fr auto;gap:18px;padding:18px 0;border-bottom:1px solid #2a2622;align-items:center}
.row .date{font-family:'JetBrains Mono',monospace;font-size:13px;color:#b89968}
.row .head{font-family:'Cormorant Garamond',serif;font-style:italic;font-size:18px}
.row .biz{font-size:11px;color:#888475;letter-spacing:.04em}
.row a{color:#b89968;text-decoration:none;font-size:9px;letter-spacing:.32em;text-transform:uppercase;border:1px solid #2a2622;padding:6px 12px}
.row a:hover{border-color:#b89968}
</style></head><body>
<h1>IG <em style="color:#f0ece2">drafts</em> · The Corridor</h1>
${drafts.map(d => `
<div class="row">
  <span class="date">${escHtml(d.date)}</span>
  <div>
    <div class="head">${escHtml(d.headline)}</div>
    <div class="biz">${escHtml(d.biz_name)} · ${escHtml(d.category || '—')} · ${d.caption_chars} chars</div>
  </div>
  <a href="${escHtml(d.date)}.html">preview →</a>
</div>`).join('')}
</body></html>`);

  console.log(`[ig-dryrun] wrote draft for ${date}`);
  console.log(`           ${jsonPath}`);
  console.log(`           ${htmlPath}`);
  console.log(`           feature_id=${draft.feature_id} · "${draft.headline}"`);
  console.log(`           ${draft.caption_chars} char caption · ${draft.tags.length} tags`);
}

main().catch(e => { console.error(e); process.exit(1); });