[object Object]

← 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 →