[object Object]

← back to New Import Viewer

Refine: bulk forced stage-only (live single-card only), in-place chip+action refresh after live write, accurate live-vs-staged confirms, live result detail

90782f6a77267a2e366491fe3ce7d0b7c942ab5f · 2026-06-09 18:13:00 -0700 · SteveStudio2

Files touched

Diff

commit 90782f6a77267a2e366491fe3ce7d0b7c942ab5f
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date:   Tue Jun 9 18:13:00 2026 -0700

    Refine: bulk forced stage-only (live single-card only), in-place chip+action refresh after live write, accurate live-vs-staged confirms, live result detail
---
 data/launch-queue.jsonl |  4 ++++
 public/index.html       | 38 +++++++++++++++++++++++++++++++-------
 server.js               |  4 ++++
 3 files changed, 39 insertions(+), 7 deletions(-)

diff --git a/data/launch-queue.jsonl b/data/launch-queue.jsonl
index e69de29..2e48011 100644
--- a/data/launch-queue.jsonl
+++ b/data/launch-queue.jsonl
@@ -0,0 +1,4 @@
+{"batchId":"unpublish-2-67187-mq7diua2","action":"unpublish","mode":"draft","count":2,"skus":2,"at":"2026-06-10T01:12:08.140Z"}
+{"batchId":"unpublish-1-67187-mq7diucc","action":"unpublish","mode":"draft","count":1,"skus":1,"at":"2026-06-10T01:12:08.220Z"}
+{"batchId":"unpublish-2-676255-mq7djm45","action":"unpublish","mode":"draft","count":2,"skus":2,"at":"2026-06-10T01:12:44.213Z"}
+{"batchId":"unpublish-1-676255-mq7djm56","action":"unpublish","mode":"draft","count":1,"skus":1,"at":"2026-06-10T01:12:44.250Z"}
diff --git a/public/index.html b/public/index.html
index 1651b10..0941a4a 100644
--- a/public/index.html
+++ b/public/index.html
@@ -148,9 +148,21 @@ input[type=range]{accent-color:var(--acc)}
 </div>
 <script>
 const $=s=>document.querySelector(s);
-const grid=$('#grid'), state={offset:0,limit:120,count:0,loading:false,live:false};
+const grid=$('#grid'), state={offset:0,limit:120,count:0,loading:false,live:false,liveActions:[],store:''};
 const sel=new Set();
 const STATUS_LABEL={NEW:'NEW',PUBLISHED:'PUBLISHED',UNPUBLISHED:'UNPUBLISHED',ARCHIVED:'ARCHIVED',DELETED:'DELETED',ON_SHOPIFY:'ON SHOPIFY'};
+// Resulting status after a live action lands (optimistic; live path returns truth).
+const ACTION_RESULT={publish:'PUBLISHED',unpublish:'UNPUBLISHED',archive:'ARCHIVED'};
+const isLive=a=>state.liveActions.includes(a);
+// Rewrite a card's status chip + action buttons in place after its state changes.
+function updateCardStatus(id,st){
+  const c=grid.querySelector('.card[data-id="'+id+'"]'); if(!c)return;
+  c.dataset.st=st;
+  const chip=c.querySelector('.b>.chips>.chip:not(.toggle)');
+  if(chip){chip.className='chip st-'+st;chip.textContent=STATUS_LABEL[st]||st;}
+  const actsEl=c.querySelector('.acts');
+  if(actsEl)actsEl.innerHTML=actionsFor(st).map(([a,label,cls])=>`<button class="act ${cls}" onclick="rowAction(${id},'${a}',this)">${label}</button>`).join('');
+}
 function syncSel(){const n=sel.size;$('#seln').textContent=n.toLocaleString()+' selected';$('#selbar').classList.toggle('on',n>0);$('#bulkgo').disabled=!n;}
 function togglePick(id,cb){if(cb.checked)sel.add(id);else sel.delete(id);cb.closest('.card').classList.toggle('sel',cb.checked);syncSel();}
 function toggleCard(el){el.closest('.card').classList.toggle('open');}
@@ -224,7 +236,7 @@ function setHeaderCount(counts){
 }
 async function boot(){
   const s=await (await fetch('/api/stats')).json();
-  state.live=s.live; const c=s.counts||{};
+  state.live=s.live; state.liveActions=s.liveActions||[]; state.store=s.store||''; const c=s.counts||{};
   setHeaderCount(c);
   [['new','🟠 New to import'],['onshopify','🔵 On Shopify (all)'],['published','🟢 Published'],['unpublished','⚪ Unpublished'],['archived','🔴 Archived'],['all','▦ All']].forEach(([k,lbl])=>{
     const o=$('#status').querySelector(`option[value="${k}"]`); if(o&&c[k]!=null) o.textContent=`${lbl} (${c[k].toLocaleString()})`;
@@ -246,12 +258,24 @@ $('#selall').onclick=()=>{grid.querySelectorAll('.card').forEach(c=>{const id=+c
 $('#clearsel').onclick=()=>{sel.clear();grid.querySelectorAll('.card.sel').forEach(c=>{c.classList.remove('sel');const cb=c.querySelector('.pick input');if(cb)cb.checked=false;});syncSel();};
 async function rowAction(id,spec,btn){
   const [action,mode]=spec.split(':');
-  if(!confirm(`${action.toUpperCase()} 1 product?\n\nThis STAGES the action to the launch queue. ${state.live?'Live writes ON but per-action push not yet wired — still stages only.':'Live Shopify writes are OFF — nothing reaches the store.'}`))return;
-  btn.disabled=true; const msg=$('#msg-'+id); if(msg)msg.textContent='staging…';
+  const live=isLive(action);
+  const warn=live
+    ? `⚠️ LIVE ${action.toUpperCase()} on ${state.store}\n\nThis writes to the REAL store immediately (reversible). Proceed?`
+    : `${action.toUpperCase()} 1 product?\n\nStages to the launch queue — nothing reaches the store.`;
+  if(!confirm(warn))return;
+  btn.disabled=true; const msg=$('#msg-'+id); if(msg)msg.textContent=live?'writing to Shopify…':'staging…';
   try{
     const r=await fetch('/api/action',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({ids:[id],action,mode})});
     const d=await r.json(); if(d.error)throw new Error(d.error);
-    if(msg)msg.textContent=`✅ staged ${action}${mode?(' ('+mode+')'):''} — batch ${d.batchId}`;
+    if(d.live){
+      const ap=d.applied||{}; const res=(d.results||[])[0]||{};
+      if(res.ok){updateCardStatus(id,res.status?res.status.toUpperCase():ACTION_RESULT[action]||'UNPUBLISHED');
+        if(msg)msg.textContent=`✅ LIVE ${action} → ${res.status||'done'} on ${d.store}`;}
+      else if(res.skipped){if(msg)msg.textContent=`↷ skipped — ${res.skipped}`;}
+      else{if(msg)msg.textContent=`✗ live ${action} failed — ${res.error||'unknown'}`;}
+    }else{
+      if(msg)msg.textContent=`✅ staged ${action}${mode?(' ('+mode+')'):''} — batch ${d.batchId}`;
+    }
   }catch(e){if(msg)msg.textContent='✗ '+e.message;}
   btn.disabled=false;
 }
@@ -259,10 +283,10 @@ window.rowAction=rowAction; window.togglePick=togglePick; window.toggleCard=togg
 $('#bulkgo').onclick=async()=>{
   const ids=[...sel]; if(!ids.length)return;
   const [action,mode]=$('#bulkact').value.split(':');
-  if(!confirm(`${action.toUpperCase()} ${ids.length} product(s)${mode?(' as '+mode.toUpperCase()):''}?\n\nStages to the launch queue. ${state.live?'Live writes ON but per-action push not yet wired — stages only.':'Live Shopify writes are OFF — nothing reaches the store.'}`))return;
+  if(!confirm(`${action.toUpperCase()} ${ids.length} product(s)${mode?(' as '+mode.toUpperCase()):''}?\n\nBulk actions ALWAYS stage to the launch queue — bulk never writes live to the store (single-card only).`))return;
   $('#bulkgo').disabled=true; $('#bulkmsg').textContent='staging…';
   try{
-    const r=await fetch('/api/action',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({ids,action,mode})});
+    const r=await fetch('/api/action',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({ids,action,mode,stageOnly:true})});
     const d=await r.json(); if(d.error)throw new Error(d.error);
     const vlist=Object.entries(d.byVendor||{}).map(([v,n])=>`${v}:${n}`).join(', ');
     $('#bulkmsg').textContent=`✅ staged ${d.staged} ${action} — ${vlist}`;
diff --git a/server.js b/server.js
index f70d967..3d7e2d3 100644
--- a/server.js
+++ b/server.js
@@ -166,6 +166,10 @@ const server = http.createServer((req, res) => {
         FROM vendor_catalog vc ${SP_JOIN}`)[0] || [];
       return send(res, 200, JSON.stringify({
         live: LIVE,
+        // Actions that would fire a live single-card write right now (master gate
+        // on AND allow-listed AND token present). Bulk never goes live regardless.
+        liveActions: LIVE && SHOP_TOKEN ? [...LIVE_ACTIONS] : [],
+        store: SHOP_STORE,
         counts: { new: +r[0] || 0, onshopify: +r[1] || 0, published: +r[2] || 0,
                   unpublished: +r[3] || 0, archived: +r[4] || 0, all: +r[5] || 0 },
         newestCrawl: r[6] || null,

← 16d4ca8 Force live-write gate OFF by default (SHOPIFY_LIVE_WRITES ma  ·  back to New Import Viewer  ·  Apply design-agent critique: red danger treatment for live a 0d6de3d →