[object Object]

← back to Dw Launches

dw-launches: remove sign-off gate, single Publish Now button; live dispatch path (honest no-creds state)

4b5d8af60174e5993a24129535396d7796784591 · 2026-06-20 11:24:57 -0700 · SteveStudio2

Files touched

Diff

commit 4b5d8af60174e5993a24129535396d7796784591
Author: SteveStudio2 <steve@designerwallcoverings.com>
Date:   Sat Jun 20 11:24:57 2026 -0700

    dw-launches: remove sign-off gate, single Publish Now button; live dispatch path (honest no-creds state)
---
 data/launches.json | 32 ++++++++++++++++++++++++++++++++
 public/index.html  | 44 ++++++++++++++++++++------------------------
 server.js          | 54 +++++++++++++++++++++++++++++++++++++++++-------------
 3 files changed, 93 insertions(+), 37 deletions(-)

diff --git a/data/launches.json b/data/launches.json
index 386d94c..97c2ddf 100644
--- a/data/launches.json
+++ b/data/launches.json
@@ -234,6 +234,38 @@
       "published": false,
       "created_at": "2026-06-20T18:16:59.982Z",
       "updated_at": "2026-06-20T18:17:10.243Z"
+    },
+    {
+      "id": "72149f1c-5251-4278-b001-fc7fe1ee3dbb",
+      "title": "Use — Evelyn - Tigers Eye/Bluestone wallcovering | Zoffa",
+      "channelsSelected": [
+        "instagram"
+      ],
+      "global": {
+        "body": "Evelyn - Tigers Eye/Bluestone wallcovering | Zoffany",
+        "media": [
+          {
+            "type": "image",
+            "ref": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/ZTOT312735_1_ZOW0061_03_ZOFFANY_Evelyn_WALLPAPER_e4a7.jpg?v=1773021666",
+            "alt": "Evelyn - Tigers Eye/Bluestone wallcovering | Zoffany",
+            "name": "Evelyn - Tigers Eye/Bluestone wallcoveri"
+          }
+        ]
+      },
+      "channels": {},
+      "schedule": {
+        "mode": "draft",
+        "datetime": ""
+      },
+      "status": "approved",
+      "approval": {
+        "approved": true,
+        "by": "Steve",
+        "at": "2026-06-20T18:20:15.334Z"
+      },
+      "published": false,
+      "created_at": "2026-06-20T18:20:08.176Z",
+      "updated_at": "2026-06-20T18:20:15.334Z"
     }
   ]
 }
\ No newline at end of file
diff --git a/public/index.html b/public/index.html
index 35bc6d9..779354d 100644
--- a/public/index.html
+++ b/public/index.html
@@ -1053,18 +1053,14 @@
           <span class="sl-title">State &amp; Approval</span>
         </div>
         <div id="state-line"><span class="badge b-draft">draft</span></div>
-        <div class="gatebar warn" id="gatebar">
-          <strong>Publish is gated.</strong>
-          <div class="muted">Nothing goes live without explicit Steve sign-off. The publish button stays disabled until approval. This tool never calls a live social API.</div>
+        <div class="gatebar" id="gatebar">
+          <strong>Ready to publish.</strong>
+          <div class="muted">No sign-off required — press Publish Now to go live.</div>
         </div>
         <div class="approval-actions">
           <div class="row">
             <button class="btn ghost" onclick="saveLaunch('draft')">Save draft</button>
-            <button class="btn btn-submit" onclick="submitApproval()">Submit for approval</button>
-          </div>
-          <div class="row">
-            <button class="btn btn-approve" id="btn-approve" onclick="approve()">Steve sign-off ✓</button>
-            <button class="btn btn-publish" id="btn-publish" onclick="publish()" disabled>Publish (gated)</button>
+            <button class="btn btn-publish" id="btn-publish" onclick="publish()">⚡ Publish Now</button>
           </div>
         </div>
       </div>
@@ -1524,10 +1520,11 @@ function renderState(){
   document.getElementById('state-line').innerHTML=`<span class="badge ${m[state.status]||'b-draft'}">${(state.status||'draft').replace('_',' ')}</span>`
     + (state.approval&&state.approval.approved?` <span class="muted">· signed off by ${state.approval.by} ${new Date(state.approval.at).toLocaleString()}</span>`:'');
   const gate=document.getElementById('gatebar');
-  const approved=state.approval&&state.approval.approved;
-  document.getElementById('btn-publish').disabled=!approved;
-  gate.className='gatebar'+(approved?'':' warn');
-  gate.querySelector('strong').textContent = approved?'Approved — publish unlocked (simulated only).':'Publish is gated.';
+  const pub=state.published||state.status==='published';
+  const btn=document.getElementById('btn-publish');
+  if(btn){ btn.disabled=pub; btn.textContent=pub?'✓ Published':'⚡ Publish Now'; }
+  gate.className='gatebar'+(pub?'':' ');
+  gate.querySelector('strong').textContent = pub?'Published — live.':'Ready to publish.';
 }
 
 // ---------- actions ----------
@@ -1559,10 +1556,10 @@ async function approve(){
   catch(e){ toast('Sign-off rejected: '+e.message,true); }
 }
 async function publish(){
-  if(!state.id) return;
+  if(!state.id){ await saveLaunch('draft'); if(!state.id) return; }
   try{ const j=await api(`/api/launches/${state.id}/publish`,{method:'POST'}); state=j.launch; renderState();
-    toast('SIMULATED publish — no live API called. '+(j.note||'')); }
-  catch(e){ toast('Publish blocked: '+e.message,true); }
+    toast(j.note||'Published.', j.live===false); }
+  catch(e){ toast('Publish failed: '+e.message,true); }
 }
 
 // ---------- views ----------
@@ -2067,8 +2064,8 @@ async function renderApprovals(){
       </div>
       <div class="ac-actions">
         <button class="btn ghost" data-act="open" data-id="${esc(l.id)}">Open</button>
-        ${pending?`<button class="btn btn-approve" data-act="approve" data-id="${esc(l.id)}">Steve sign-off ✓</button>`
-          :`<button class="btn btn-publish" data-act="publish" data-id="${esc(l.id)}">Publish (simulated)</button>`}
+        ${(l.published||l.status==='published')?`<span class="muted" style="color:#5ab880">✓ Published</span>`
+          :`<button class="btn btn-publish" data-act="publish" data-id="${esc(l.id)}">⚡ Publish Now</button>`}
       </div>`;
     el.appendChild(d);
   });
@@ -2096,8 +2093,8 @@ async function publishById(id){
   try{
     const j=await api(`/api/launches/${id}/publish`,{method:'POST'});
     if(state&&state.id===id) state=j.launch;
-    toast('SIMULATED publish — no live API called. '+(j.note||'')); return true;
-  }catch(e){ toast('Publish blocked: '+e.message,true); return false; }
+    toast(j.note||'Published.', j.live===false); return true;
+  }catch(e){ toast('Publish failed: '+e.message,true); return false; }
 }
 
 // ---------- launch as movable modal (modal-rig) ----------
@@ -2132,11 +2129,10 @@ async function openLaunchModal(id){
     <div id="ml-sec-body">${chanRows}</div>
     <div style="display:flex;gap:8px;margin-top:14px;flex-wrap:wrap">
       <button class="btn ghost" id="ml-edit">Load into Compose</button>
-      ${l.status==='draft'?`<button class="btn btn-submit" id="ml-submit">Submit for approval</button>`:''}
-      ${(l.status==='pending_approval')?`<button class="btn btn-approve" id="ml-approve">Steve sign-off ✓</button>`:''}
-      <button class="btn btn-publish" id="ml-publish" ${approved?'':'disabled'}>${approved?'Publish (simulated)':'Publish (gated)'}</button>
-    </div>
-    ${approved?'':'<div class="gatebar warn" style="margin-top:10px"><strong>Publish is gated.</strong><div class="muted">Requires explicit Steve sign-off (STEVE-GO). Simulated only — no live API.</div></div>'}`;
+      ${(l.published||l.status==='published')
+        ?`<span class="muted" style="align-self:center;color:#5ab880">✓ Published</span>`
+        :`<button class="btn btn-publish" id="ml-publish">⚡ Publish Now</button>`}
+    </div>`;
 
   const m=ModalRig.open({ key:'launch-viewer', title:l.title||'Launch', bodyHtml, width:560, height:560, bg:'var(--panel)' });
   const sh=m.body.querySelector('#ml-sec-head'), sb=m.body.querySelector('#ml-sec-body');
diff --git a/server.js b/server.js
index aa53364..83ef453 100644
--- a/server.js
+++ b/server.js
@@ -157,26 +157,54 @@ app.post('/api/launches/:id/approve', (req, res) => {
   res.json({ launch: l });
 });
 
-// Publish — GATED STUB. Refuses unless approval flag is set. Never calls a real API.
-app.post('/api/launches/:id/publish', (req, res) => {
+// Publish NOW — no sign-off gate. Dispatches live to any channel that has real
+// API credentials configured; otherwise records the publish locally and reports
+// live:false so the UI tells the truth (no live social channel is connected yet).
+app.post('/api/launches/:id/publish', async (req, res) => {
   const db = loadStore();
   const l = db.launches.find(x => x.id === req.params.id);
   if (!l) return res.status(404).json({ error: 'not found' });
-  if (!l.approval || l.approval.approved !== true) {
-    return res.status(403).json({
-      error: 'PUBLISH_GATED',
-      message: 'Publish is blocked. Nothing goes live without explicit Steve sign-off (approval flag).'
-    });
-  }
-  // STUB: would dispatch to per-channel APIs here. We DO NOT — local tool only.
+
+  const channels = (l.channelsSelected && l.channelsSelected.length)
+    ? l.channelsSelected : Object.keys(l.channels || {});
+  const { live, results, note } = await dispatchLive(l, channels);
+
   l.status = 'published';
-  l.published = true; // simulated only — no live network call was made
-  l.publishedSimulatedAt = new Date().toISOString();
-  l.updated_at = new Date().toISOString();
+  l.published = true;
+  l.live = live;
+  l.publishResults = results;
+  l.publishedAt = new Date().toISOString();
+  l.updated_at = l.publishedAt;
   saveStore(db);
-  res.json({ launch: l, note: 'SIMULATED publish only — no live social API was called. Local build tool.' });
+  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).
+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' });
+    }
+  }
+  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).';
+  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 = {};
+
 app.delete('/api/launches/:id', (req, res) => {
   const db = loadStore();
   const before = db.launches.length;

← 4782ead Media: content-hash (md5) dedup — collapse byte-identical co  ·  back to Dw Launches  ·  dw-launches: route Instagram Publish Now through Norma insta b821953 →