[object Object]

← back to Gated Queue Runner

GO actions: Allow/Never-run(archive)/Run-now(iTerm2 per item) + select-all-shown

dac79e4ba440d3b0180563c92099270826241aa3 · 2026-08-18 13:27:01 -0700 · steve

Files touched

Diff

commit dac79e4ba440d3b0180563c92099270826241aa3
Author: steve <steve@designerwallcoverings.com>
Date:   Tue Aug 18 13:27:01 2026 -0700

    GO actions: Allow/Never-run(archive)/Run-now(iTerm2 per item) + select-all-shown
---
 index.html | 28 ++++++++++++++++++++++++----
 server.js  | 30 ++++++++++++++++++++++++++++++
 2 files changed, 54 insertions(+), 4 deletions(-)

diff --git a/index.html b/index.html
index 435dc0f..e1816e2 100644
--- a/index.html
+++ b/index.html
@@ -67,6 +67,7 @@
       </select>
     </label>
     <label>Size <input type="range" id="dens" min="1" max="5" value="3"></label>
+    <button id="allshown" style="border:1px solid #ccd3e0;background:#fff;border-radius:8px;padding:6px 10px;cursor:pointer;font-size:.85em">☑ select all shown</button>
   </div>
   <div class="chips" id="chips"></div>
 </header>
@@ -74,10 +75,13 @@
   <div class="grid" id="grid"></div>
   <table id="tbl" style="display:none"><thead id="thead"></thead><tbody id="tbody"></tbody></table>
 </main>
-<div id="selbar" style="display:none;position:fixed;bottom:0;left:0;right:0;background:#1c2333;color:#fff;padding:12px 18px;display:none;align-items:center;gap:14px;box-shadow:0 -2px 10px rgba(0,0,0,.2);z-index:20">
-  <span id="selcount" style="font-weight:700"></span>
-  <button id="savebtn" style="background:#2d5bff;color:#fff;border:none;border-radius:8px;padding:8px 16px;font-size:.95em;cursor:pointer">💾 Save my list → hand to Claude</button>
-  <button id="clearbtn" style="background:#39415a;color:#fff;border:none;border-radius:8px;padding:8px 14px;cursor:pointer">clear</button>
+<div id="selbar" style="display:none;position:fixed;bottom:0;left:0;right:0;background:#1c2333;color:#fff;padding:12px 18px;align-items:center;gap:10px;flex-wrap:wrap;box-shadow:0 -2px 10px rgba(0,0,0,.25);z-index:20">
+  <span id="selcount" style="font-weight:700;margin-right:6px"></span>
+  <button class="act" data-k="allow" style="background:#1f9d55;color:#fff;border:none;border-radius:8px;padding:9px 16px;font-size:.95em;cursor:pointer">✅ Allow (do later)</button>
+  <button class="act" data-k="run" style="background:#2d5bff;color:#fff;border:none;border-radius:8px;padding:9px 16px;font-size:.95em;cursor:pointer">▶️ Run now (iTerm2)</button>
+  <button class="act" data-k="never" style="background:#b23b3b;color:#fff;border:none;border-radius:8px;padding:9px 16px;font-size:.95em;cursor:pointer">🚫 Never run (archive)</button>
+  <button id="savebtn" style="background:#39415a;color:#fff;border:none;border-radius:8px;padding:9px 14px;cursor:pointer">💾 Save list</button>
+  <button id="clearbtn" style="background:#39415a;color:#fff;border:none;border-radius:8px;padding:9px 12px;cursor:pointer">clear</button>
   <span id="saveok" style="color:#7CFFB0"></span>
 </div>
 <dialog id="dlg"><div class="dh"><span id="dt"></span><button class="x" onclick="dlg.close()">close ✕</button></div><pre id="dp"></pre></dialog>
@@ -98,6 +102,22 @@ $('savebtn').onclick=()=>{
     .then(r=>r.json()).then(d=>{ $('saveok').textContent=`saved ${d.count} — tell Claude "do my list"`; setTimeout(()=>$('saveok').textContent='',6000); });
 };
 $('clearbtn').onclick=()=>{ SEL.clear(); saveSel(); bar(); render(); };
+$('allshown').onclick=()=>{ getRows().forEach(x=>SEL.add(x.file)); saveSel(); bar(); render(); };
+document.querySelectorAll('.act').forEach(btn=>btn.onclick=()=>{
+  const k=btn.dataset.k, files=[...SEL];
+  if(!files.length) return;
+  const verb={allow:'ALLOW (move to the do-later pile)', run:`RUN NOW — this opens ${files.length} iTerm2 window${files.length>1?'s':''}`, never:'NEVER RUN (archive out of the queue)'}[k];
+  if(!confirm(`${verb}\n\n${files.length} item${files.length>1?'s':''}. Continue?`)) return;
+  if(k==='run' && files.length>10 && !confirm(`That is ${files.length} windows at once. Really open all of them?`)) return;
+  $('saveok').textContent='working…';
+  fetch('/api/'+k,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({files})})
+    .then(r=>r.json()).then(d=>{
+      $('saveok').textContent = k==='run' ? `opened ${d.done} window(s)` : `${k==='allow'?'allowed':'archived'} ${d.done}`;
+      if(k!=='run'){ files.forEach(f=>SEL.delete(f)); saveSel(); }   // acted items leave the queue
+      else { files.forEach(f=>SEL.delete(f)); saveSel(); }
+      setTimeout(()=>{ $('saveok').textContent=''; load(); }, 1200);   // reload list (acted items gone)
+    }).catch(()=>{ $('saveok').textContent='error'; });
+});
 
 function fmtWhen(ms){ return new Date(ms).toLocaleString(undefined,{year:'numeric',month:'short',day:'numeric',hour:'numeric',minute:'2-digit'}); }
 
diff --git a/server.js b/server.js
index 3025394..f2339d0 100644
--- a/server.js
+++ b/server.js
@@ -5,6 +5,7 @@ const http = require('http');
 const fs = require('fs');
 const os = require('os');
 const path = require('path');
+const { execFile } = require('child_process');
 
 const DIR = path.join(os.homedir(), '.claude/yolo-queue/pending-approval');
 const PORT = parseInt(fs.readFileSync(path.join(__dirname, '.port'), 'utf8').trim() || '9801', 10);
@@ -141,6 +142,35 @@ const server = http.createServer((req, res) => {
     });
     return;
   }
+  // --- action endpoints: allow (greenlight→_approved), never (archive→_never), run (iTerm2 per item) ---
+  if ((req.url === '/api/allow' || req.url === '/api/never' || req.url === '/api/run') && req.method === 'POST') {
+    const kind = req.url.split('/')[2];
+    let b = ''; req.on('data', c => b += c); req.on('end', () => {
+      let files = [];
+      try { files = (JSON.parse(b).files || []).map(f => path.basename(f)); } catch {}
+      let done = 0;
+      if (kind === 'run') {
+        for (const f of files) {
+          const abs = path.join(DIR, f);
+          if (!fs.existsSync(abs)) continue;
+          const prompt = ('Steve approved working this gated queue item via the queue viewer. FIRST verify the current state — it may already be done; if so, confirm and stop. Otherwise do the REVERSIBLE parts end-to-end, but DRAFT any hard-gated externality (spend, send-to-list, DNS, publish, prod deploy, remote push) to ~/.claude/yolo-queue/pending-approval/ instead of firing it. Show $ costs, ledger reversible work, verify, and report. Memo: ' + abs).replace(/["\\]/g, '\\$&');
+          const osa = ['tell application "iTerm2"','activate','create window with default profile',
+            'tell current session of current window','write text "claude \\"' + prompt + '\\""','end tell','end tell'].join('\n');
+          try { execFile('osascript', ['-e', osa], () => {}); done++; } catch {}
+        }
+      } else {
+        const sub = kind === 'allow' ? '_approved' : '_never';
+        const dest = path.join(DIR, sub); try { fs.mkdirSync(dest, { recursive: true }); } catch {}
+        for (const f of files) {
+          const src = path.join(DIR, f);
+          if (fs.existsSync(src) && src.startsWith(DIR)) { try { fs.renameSync(src, path.join(dest, f)); done++; } catch {} }
+        }
+      }
+      res.writeHead(200, { 'Content-Type': 'application/json' });
+      res.end(JSON.stringify({ ok: true, kind, done }));
+    });
+    return;
+  }
   if (req.url.startsWith('/api/memo')) {
     const f = decodeURIComponent((req.url.split('?f=')[1] || ''));
     const p = path.join(DIR, path.basename(f));

← 5efaf26 add sortable Table view + Cards/Table toggle (every column c  ·  back to Gated Queue Runner  ·  add List view + full 8-column sortable Table (every field cl c0ab2e8 →