← back to Marketing Command Center
modules/quickpost/index.js
90 lines
// Quick Post — easy per-platform "post" buttons across the command center. By
// design this STAGES A DRAFT (gated): a click records a draft post; nothing goes
// live without a human confirm. Live publishing is wired to the channels layer
// but only fires once a platform's token is connected AND confirm:true is passed
// — until then every platform falls back to a staged draft. Draft-only is the
// safe default that honors the standing "social posting is Steve-gated" rule.
const fs = require('fs');
const path = require('path');
const DATA_DIR = path.join(__dirname, '..', '..', 'data');
const STORE = path.join(DATA_DIR, 'quickpost-drafts.json');
const PLATFORMS = [
{ id: 'instagram', label: 'Instagram', icon: '📷' },
{ id: 'tiktok', label: 'TikTok', icon: '🎵' },
{ id: 'facebook', label: 'Facebook', icon: '📘' },
{ id: 'linkedin', label: 'LinkedIn', icon: '💼' },
];
function load() { try { return JSON.parse(fs.readFileSync(STORE, 'utf8')); } catch { return []; } }
function save(list) {
try { fs.mkdirSync(DATA_DIR, { recursive: true }); fs.writeFileSync(STORE, JSON.stringify(list, null, 2)); }
catch (e) { /* non-fatal */ }
}
// stable-ish id without Date.now in hot path is fine here (server runtime, not a composition)
function newId() { return 'qp_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 7); }
module.exports = {
id: 'quickpost',
title: 'Make a Post',
icon: '✨',
mount(router) {
// per-platform connection state (best-effort). Tokens aren't wired yet, so
// everything reports draft-only; the front-end shows this honestly.
router.get('/status', (_req, res) => {
res.json({
platforms: PLATFORMS.map(p => ({ ...p, connected: false })),
note: 'Draft-only: posts are staged for approval. Live posting activates per platform once its token is connected.',
draftCount: load().length,
});
});
router.get('/drafts', (_req, res) => res.json({ drafts: load().slice().reverse() }));
// Stage a draft. This is the "easy post" action — always safe, always gated.
router.post('/draft', (req, res) => {
const b = req.body || {};
const platform = String(b.platform || '').toLowerCase();
if (!PLATFORMS.some(p => p.id === platform)) return res.status(400).json({ error: 'unknown platform' });
const entry = {
id: newId(),
platform,
handle: String(b.handle || '').replace(/^@/, '').slice(0, 100), // which DW-owned IG account this draft targets
caption: String(b.caption || '').slice(0, 2200),
mediaUrl: String(b.mediaUrl || ''),
source: String(b.source || 'quickpost'),
status: 'draft',
created_at: new Date().toISOString(),
};
const list = load(); list.push(entry); save(list);
res.json({ ok: true, staged: true, id: entry.id, platform });
});
// Remove a staged draft.
router.post('/draft/delete', (req, res) => {
const id = String((req.body || {}).id || '');
const list = load().filter(d => d.id !== id);
save(list);
res.json({ ok: true });
});
// Live publish — GATED. Requires an explicit confirm AND a connected platform.
// No platform is connected yet, so this always falls back to a staged draft.
router.post('/publish', (req, res) => {
const b = req.body || {};
const platform = String(b.platform || '').toLowerCase();
// (future) if channels.status[platform].connected && b.confirm === true → real post
const entry = {
id: newId(), platform, handle: String(b.handle || '').replace(/^@/, '').slice(0, 100),
caption: String(b.caption || '').slice(0, 2200),
mediaUrl: String(b.mediaUrl || ''), source: String(b.source || 'quickpost'),
status: 'draft', created_at: new Date().toISOString(),
};
const list = load(); list.push(entry); save(list);
res.json({ ok: true, staged: true, live: false, id: entry.id,
reason: 'platform not connected — staged as a draft for approval' });
});
},
};