← back to Prestige Car Wash
reports/deployed-20260720/publish-due.js
71 lines
'use strict';
/**
* Prestige social scheduler worker — Phase 1 (DARK / INERT).
*
* Reads data/schedule.json, finds posts that are DUE (when <= now) and not yet
* sent, and hands each to its platform adapter. Adapters are "inert until keyed":
* with no token in .env they return {skipped:'needs-connection'} and the post
* STAYS queued — i.e. exactly today's manual copy&open behavior. Every attempt is
* appended to reports/social-posts.jsonl.
*
* Nothing is auto-posted in Phase 1. Real posting only happens once a platform
* adapter is IMPLEMENTED, keyed, AND its go-live is approved
* (see reports/SCOPE-social-autopost-*.md). Yelp has no posting API — manual forever.
*
* Not yet installed on any timer — run manually: `node scripts/publish-due.js`.
*/
require('dotenv').config();
const fs = require('fs');
const path = require('path');
const DATA = path.join(__dirname, '..', 'data');
const REPORTS = path.join(__dirname, '..', 'reports');
const FILE = path.join(DATA, 'schedule.json');
// One adapter per platform, each gated behind an env token (Stripe-style inert).
function needsKey(env) {
return async (_post) => {
if (!process.env[env]) return { ok: false, skipped: 'needs-connection', env };
// Adapter body intentionally NOT implemented in Phase 1 — go-live is gated.
return { ok: false, skipped: 'adapter-not-enabled', env };
};
}
const adapters = {
'Instagram': needsKey('PCW_IG_ACCESS_TOKEN'),
'Facebook Page': needsKey('PCW_FB_PAGE_TOKEN'),
'TikTok': needsKey('PCW_TIKTOK_ACCESS_TOKEN'),
'YouTube': needsKey('PCW_YT_REFRESH_TOKEN'),
'Google Business Profile': needsKey('PCW_GBP_ACCESS_TOKEN')
// 'Yelp Business': no posting API — always manual, no adapter.
};
function log(entry) {
try { fs.mkdirSync(REPORTS, { recursive: true }); } catch (e) {}
fs.appendFileSync(path.join(REPORTS, 'social-posts.jsonl'),
JSON.stringify({ ...entry, at: new Date().toISOString() }) + '\n');
}
async function main() {
let doc;
try { doc = JSON.parse(fs.readFileSync(FILE, 'utf8')); }
catch (e) { console.log('[publish-due] no schedule.json — nothing to do'); return; }
const posts = Array.isArray(doc.posts) ? doc.posts : [];
const now = Date.now();
let due = 0, attempted = 0;
for (const p of posts) {
if (p.status === 'sent') continue;
if (!p.when || new Date(p.when).getTime() > now) continue;
due++;
for (const plat of (p.socials || [])) {
const fn = adapters[plat];
const r = fn ? await fn(p) : { ok: false, skipped: 'no-adapter (manual only)' };
attempted++;
log({ post_id: p.id, platform: plat, result: r });
}
// Phase 1: nothing actually posts, so status is NOT flipped to 'sent' — the
// post stays visible in the admin as "ready to post" (manual copy&open) until
// an adapter is enabled + approved.
}
console.log(`[publish-due] ${posts.length} queued · ${due} due · ${attempted} adapter-calls · 0 auto-posted (Phase 1 dark)`);
}
main();