← back to Dw Launches
dw-launches: route Instagram Publish Now through Norma instagram-agent (:9810), honest live/simulated reporting
b8219531d5788da2becf6c5c958bfd6223817a6c · 2026-06-20 11:31:59 -0700 · SteveStudio2
Files touched
Diff
commit b8219531d5788da2becf6c5c958bfd6223817a6c
Author: SteveStudio2 <steve@designerwallcoverings.com>
Date: Sat Jun 20 11:31:59 2026 -0700
dw-launches: route Instagram Publish Now through Norma instagram-agent (:9810), honest live/simulated reporting
---
server.js | 62 ++++++++++++++++++++++++++++++++++++++++++++++++--------------
1 file changed, 48 insertions(+), 14 deletions(-)
diff --git a/server.js b/server.js
index 83ef453..9da996e 100644
--- a/server.js
+++ b/server.js
@@ -179,31 +179,65 @@ app.post('/api/launches/:id/publish', async (req, res) => {
res.json({ launch: l, live, results, note });
});
-// Live dispatch. Each channel posts ONLY if its real credentials are present in
-// the environment. None are wired today, so this returns live:false with an
-// honest note rather than pretending. Add per-channel posting here + creds to
-// go truly live (e.g. Instagram via Meta Graph Content Publishing).
+// Live dispatch. Each channel handler returns { posted, ... }. A post only
+// counts as live when the downstream actually posted (not when it simulated),
+// so the UI tells the truth instead of pretending.
async function dispatchLive(launch, channels) {
const results = [];
let anyLive = false;
for (const ch of channels) {
const handler = LIVE_CHANNELS[ch];
- if (handler && handler.ready()) {
- try { const r = await handler.post(launch); results.push({ channel: ch, ok: true, ...r }); anyLive = true; }
- catch (e) { results.push({ channel: ch, ok: false, error: String(e.message || e) }); }
- } else {
- results.push({ channel: ch, ok: false, skipped: true, reason: 'no live credentials configured' });
+ if (!handler) { results.push({ channel: ch, ok: false, skipped: true, reason: 'no live integration for this channel yet' }); continue; }
+ try {
+ const r = await handler(launch);
+ if (r.posted) { anyLive = true; results.push({ channel: ch, ok: true, ...r }); }
+ else { results.push({ channel: ch, ok: false, skipped: true, ...r }); }
+ } catch (e) {
+ results.push({ channel: ch, ok: false, error: String(e.message || e) });
}
}
+ const liveChans = results.filter(r => r.ok).map(r => r.channel);
const note = anyLive
- ? `Published live to ${results.filter(r => r.ok).map(r => r.channel).join(', ') || 'channels'}.`
- : 'Published — recorded locally. No live social channel is connected yet (add channel API credentials to post to real accounts).';
+ ? `Published live to ${liveChans.join(', ')}.`
+ : 'Published — recorded. ' + (results.map(r => r.reason).find(Boolean)
+ || 'No live social channel posted (connect a channel to post to real accounts).');
return { live: anyLive, results, note };
}
-// Per-channel live posters — empty until real credentials + posting code land.
-// Example shape: instagram: { ready: () => !!process.env.IG_ACCESS_TOKEN, post: async (l) => {...} }
-const LIVE_CHANNELS = {};
+// Per-channel live posters. Instagram routes through Norma's instagram-agent
+// (Meta Graph Content Publishing) at NORMA_URL. Norma stays in simulation until
+// IG creds + its real-post code are enabled — until then this reports posted:false
+// with an honest reason rather than faking a live post.
+const NORMA_URL = process.env.NORMA_URL || 'http://127.0.0.1:9810';
+const NORMA_AUTH = process.env.NORMA_AUTH || 'admin:'; // user:pass for Norma basic-auth
+const LIVE_CHANNELS = {
+ instagram: async (launch) => {
+ const ig = (launch.channels && launch.channels.instagram) || {};
+ const media = (ig.media && ig.media.length ? ig.media : (launch.global && launch.global.media)) || [];
+ const image_url = (media[0] && media[0].ref) || '';
+ const caption = ig.body || (launch.global && launch.global.body) || '';
+ let r;
+ try {
+ r = await fetch(`${NORMA_URL}/api/skill/post`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: 'Basic ' + Buffer.from(NORMA_AUTH).toString('base64'),
+ },
+ body: JSON.stringify({ image_url, caption }),
+ });
+ } catch (e) {
+ return { posted: false, reason: `Norma (instagram-agent :9810) is unreachable — start it to route Instagram posts (${e.code || e.message}).` };
+ }
+ if (!r.ok) return { posted: false, reason: `Norma returned HTTP ${r.status}.` };
+ const j = await r.json().catch(() => ({}));
+ const res = (j && j.result) || {};
+ if (res.simulated || res.posted === false) {
+ return { posted: false, simulated: true, permalink: res.permalink || null, reason: 'Norma is in simulation mode (no live IG credentials / real-post code not enabled).' };
+ }
+ return { posted: true, permalink: res.permalink || null, media_id: res.media_id || null, via: 'norma' };
+ },
+};
app.delete('/api/launches/:id', (req, res) => {
const db = loadStore();
← 4b5d8af dw-launches: remove sign-off gate, single Publish Now button
·
back to Dw Launches
·
auto-save: 2026-06-22T10:05:12 (1 files) — data/launches.jso 3e7b50c →