← 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 => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[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); });