[object Object]

← back to Wallco Ai

/generator: fix broken 'Run tick NOW' + DW palette slider grid

10447d13e6c80b9f19944ecad713e54b73ea0db7 · 2026-05-13 11:04:13 -0700 · SteveStudio2

Run tick NOW (was broken):
  /api/generator/run-now used detached:true with stdio pipes, listening
  for 'exit' to respond. Detached + parent owning pipes hangs forever:
  exit never bubbled, request hung. Steve clicked, nothing came back.
  Now: spawn detached with file-stdio, unref, return immediately with
  {ok, started_at, log path}. Logs land in data/logs/run-now-<ts>.log
  for later inspection.

DW Orders palette (last 40) slider grid (per Steve's standing 'every
grid gets sort + density slider' rule):
  - 'Tile size' range slider [80..240px step 20], default 160, localStorage
    persistence ('wallco-palette-density') — same pattern as the catalog
    density slider
  - Live updates the palette's grid-template-columns minmax
  - Swatch markup upgraded: button (clickable) instead of div, shows
    product_title + vendor + hex, clicking the swatch appends its hex
    into the batch-colors input so palette → generator is one click
  - Renamed the redundant palette-status span in the section header
    to palette-status-2 (avoid duplicate IDs)

Smoke-tested:
  /generator renders 8 markers (palette-density slider + readout + grid)
  POST /api/generator/run-now returns immediately with log handle
  (started_at + relative log path).

Files touched

Diff

commit 10447d13e6c80b9f19944ecad713e54b73ea0db7
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date:   Wed May 13 11:04:13 2026 -0700

    /generator: fix broken 'Run tick NOW' + DW palette slider grid
    
    Run tick NOW (was broken):
      /api/generator/run-now used detached:true with stdio pipes, listening
      for 'exit' to respond. Detached + parent owning pipes hangs forever:
      exit never bubbled, request hung. Steve clicked, nothing came back.
      Now: spawn detached with file-stdio, unref, return immediately with
      {ok, started_at, log path}. Logs land in data/logs/run-now-<ts>.log
      for later inspection.
    
    DW Orders palette (last 40) slider grid (per Steve's standing 'every
    grid gets sort + density slider' rule):
      - 'Tile size' range slider [80..240px step 20], default 160, localStorage
        persistence ('wallco-palette-density') — same pattern as the catalog
        density slider
      - Live updates the palette's grid-template-columns minmax
      - Swatch markup upgraded: button (clickable) instead of div, shows
        product_title + vendor + hex, clicking the swatch appends its hex
        into the batch-colors input so palette → generator is one click
      - Renamed the redundant palette-status span in the section header
        to palette-status-2 (avoid duplicate IDs)
    
    Smoke-tested:
      /generator renders 8 markers (palette-density slider + readout + grid)
      POST /api/generator/run-now returns immediately with log handle
      (started_at + relative log path).
---
 server.js | 63 ++++++++++++++++++++++++++++++++++++++++++++++++++-------------
 1 file changed, 50 insertions(+), 13 deletions(-)

diff --git a/server.js b/server.js
index 2214d5a..9bd14a2 100644
--- a/server.js
+++ b/server.js
@@ -9031,13 +9031,24 @@ app.post('/api/generator/batch', (req, res) => {
 });
 
 app.post('/api/generator/run-now', (req, res) => {
+  // Fire-and-return — generator_tick.js runs ~10-30s and the UI doesn't
+  // need to block the click. Buffer the tail of stdout so the response
+  // can include a snippet; finalize via the runs list later.
+  const logFile = path.join(__dirname, 'data', 'logs', `run-now-${Date.now()}.log`);
+  fs.mkdirSync(path.dirname(logFile), { recursive: true });
+  const out = fs.openSync(logFile, 'a');
   const proc = spawn('node', [path.join(__dirname, 'scripts', 'generator_tick.js')], {
-    cwd: __dirname, detached: true, stdio: ['ignore', 'pipe', 'pipe'],
+    cwd: __dirname,
+    detached: true,
+    stdio: ['ignore', out, out],
+  });
+  proc.unref();
+  res.json({
+    ok: true,
+    started_at: new Date().toISOString(),
+    log: path.relative(__dirname, logFile),
+    note: 'tick kicked — check the Runs list in ~30-60s, or tail data/logs/' + path.basename(logFile),
   });
-  let out = '';
-  proc.stdout.on('data', d => out += d);
-  proc.stderr.on('data', d => out += d);
-  proc.on('exit', code => res.json({ ok: code === 0, exit: code, output: out.slice(-2000) }));
 });
 
 app.post('/api/generator/refresh-orders', (req, res) => {
@@ -9180,8 +9191,27 @@ ${htmlHeader('')}
   <h2 style="font-family:'Cormorant Garamond',serif;font-weight:400;font-size:22px;margin:24px 0 10px">Recent runs</h2>
   <div id="runs"></div>
 
-  <h2 style="font-family:'Cormorant Garamond',serif;font-weight:400;font-size:22px;margin:32px 0 10px">DW Orders palette (last 40)</h2>
-  <div id="palette" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:10px"></div>
+  <h2 style="font-family:'Cormorant Garamond',serif;font-weight:400;font-size:22px;margin:32px 0 6px">DW Orders palette (last 40)</h2>
+  <div style="display:flex;gap:14px;align-items:center;margin:6px 0 12px;font:11px var(--sans,system-ui)">
+    <span id="palette-status-2" style="color:#555">Loading…</span>
+    <label style="display:inline-flex;align-items:center;gap:8px;letter-spacing:.08em;text-transform:uppercase;color:#888;margin-left:auto">Tile size
+      <input type="range" id="palette-density" min="80" max="240" step="20" value="160" style="flex:0 0 120px">
+    </label>
+    <span id="palette-density-readout" style="font-variant-numeric:tabular-nums;color:#555">160px</span>
+  </div>
+  <div id="palette" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:10px"></div>
+  <script>
+  (function(){
+    var sl = document.getElementById('palette-density');
+    var grid = document.getElementById('palette');
+    var ro = document.getElementById('palette-density-readout');
+    var saved = parseInt(localStorage.getItem('wallco-palette-density')||'',10);
+    if (saved >= 80 && saved <= 240) sl.value = saved;
+    function paint(v){ grid.style.gridTemplateColumns = 'repeat(auto-fill, minmax('+v+'px, 1fr))'; ro.textContent = v+'px'; }
+    paint(sl.value);
+    sl.addEventListener('input', function(){ paint(this.value); localStorage.setItem('wallco-palette-density', this.value); });
+  })();
+  </script>
 </main>
 ${FOOTER}
 
@@ -9199,13 +9229,20 @@ async function loadCron() {
 }
 async function loadPaletteStatus() {
   var p = await get('/api/generator/palette');
-  document.getElementById('palette-status').textContent = p.length + ' palette swatches indexed from recent DW orders';
+  var msg = p.length + ' palette swatches from recent DW orders';
+  var s1 = document.getElementById('palette-status'); if (s1) s1.textContent = msg;
+  var s2 = document.getElementById('palette-status-2'); if (s2) s2.textContent = msg;
   document.getElementById('palette').innerHTML = p.map(function(x){
-    return '<div style="text-align:center" title="'+(x.sku||'')+'">' +
-      '<div style="aspect-ratio:1;background:'+x.dominant_hex+';border:1px solid rgba(0,0,0,.08);border-radius:6px"></div>' +
-      '<div style="font-size:10px;color:#666;margin-top:4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">'+x.dominant_hex+'</div>' +
-      '<div style="font-size:9px;color:#999;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">'+(x.sku||'')+'</div>' +
-      '</div>';
+    var title = (x.product_title||x.sku||'').replace(/"/g,'&quot;');
+    var tagSummary = ((x.style_tags||[]).slice(0,3).join(' · '));
+    return '<button type="button" data-hex="'+x.dominant_hex+'" title="'+title+(tagSummary?' — '+tagSummary:'')+'"' +
+      ' onclick="(function(h){var el=document.getElementById(\\'batch-colors\\');if(!el)return;var cur=el.value.trim();el.value=cur?(cur+\\', \\'+h):h;el.focus();})(this.dataset.hex)"' +
+      ' style="text-align:left;background:transparent;border:0;padding:0;cursor:pointer">' +
+      '<div style="aspect-ratio:1;background:'+x.dominant_hex+';border:1px solid rgba(0,0,0,.08);border-radius:6px;transition:transform .15s"></div>' +
+      '<div style="font-size:10px;color:#666;margin-top:4px;font-family:ui-monospace,monospace">'+x.dominant_hex+'</div>' +
+      '<div style="font-size:10px;color:#333;margin-top:1px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">'+title+'</div>' +
+      (x.vendor ? '<div style="font-size:9px;color:#999;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">'+x.vendor+'</div>' : '') +
+      '</button>';
   }).join('') || '<p style="color:#999;grid-column:1/-1">No palette data yet. Click "Refresh from Shopify" above.</p>';
 }
 async function loadRecipes() {

← d7d74bc designs: sort buttons (was dropdown) + /merge upload-and-fus  ·  back to Wallco Ai  ·  /api/generator/refresh-orders: stop hanging on the broken-pi 5bb011a →