← back to Gated Queue Runner
real per-memo kid-summaries + checkbox selection → selected.json
1a013f7b6a074f0e291bd1ed501c04a249c3811a · 2026-08-18 13:18:26 -0700 · steve
Files touched
M index.htmlA selected.jsonM server.js
Diff
commit 1a013f7b6a074f0e291bd1ed501c04a249c3811a
Author: steve <steve@designerwallcoverings.com>
Date: Tue Aug 18 13:18:26 2026 -0700
real per-memo kid-summaries + checkbox selection → selected.json
---
index.html | 30 +++++++++++++++++++++++++++---
selected.json | 5 +++++
server.js | 47 ++++++++++++++++++++++++++++++++++++++++++++---
3 files changed, 76 insertions(+), 6 deletions(-)
diff --git a/index.html b/index.html
index 0bf3922..d389305 100644
--- a/index.html
+++ b/index.html
@@ -22,6 +22,9 @@
main { padding: 16px 18px; }
.grid { display: grid; grid-template-columns: repeat(var(--cols), 1fr); gap: 12px; }
.card { background: #fff; border: 1px solid #e6eaf2; border-radius: 14px; padding: 14px; display: flex; flex-direction: column; gap: 8px; box-shadow: 0 1px 3px rgba(20,30,60,.05); }
+ .card.sel { outline: 3px solid #2d5bff; outline-offset: -1px; }
+ .card .pick { font-size: .72em; color: #2d5bff; font-weight: 700; display: flex; align-items: center; gap: 3px; cursor: pointer; margin-right: auto; }
+ .card .pick input { width: 16px; height: 16px; }
.card .top { display: flex; align-items: center; gap: 9px; }
.card .emoji { font-size: 1.8em; line-height: 1; }
.card .cat { font-size: .72em; font-weight: 700; text-transform: uppercase; letter-spacing: .04em; color: #2d5bff; }
@@ -54,19 +57,37 @@
<div class="chips" id="chips"></div>
</header>
<main><div class="grid" id="grid"></div></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>
+ <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>
<script>
const KID = { spend:'💵', send:'✉️', dns:'🌐', shopify:'🛍️', catalog:'📦', deploy:'🚀', ga4:'📊', blocked:'🔒', other:'📄' };
let DATA = { gates: [], counts: {}, total: 0 };
let active = new Set(); // active category filters (empty = all)
+let SEL = new Set(JSON.parse(localStorage.gqSel || '[]'));
const $ = id => document.getElementById(id);
+function saveSel(){ localStorage.gqSel = JSON.stringify([...SEL]); }
+function bar(){
+ const b=$('selbar');
+ if(SEL.size){ b.style.display='flex'; $('selcount').textContent=`✅ ${SEL.size} picked`; }
+ else b.style.display='none';
+}
+$('savebtn').onclick=()=>{
+ fetch('/api/select',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({files:[...SEL]})})
+ .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(); };
function fmtWhen(ms){ return new Date(ms).toLocaleString(undefined,{year:'numeric',month:'short',day:'numeric',hour:'numeric',minute:'2-digit'}); }
function load(){
fetch('/api/gates').then(r=>r.json()).then(d=>{
- DATA=d; $('sub').textContent = `${d.total} things need your OK. Tap a card to read the grown-up version.`;
- renderChips(); render();
+ DATA=d; $('sub').textContent = `${d.total} things need your OK. Check the boxes for the ones you want done, then Save.`;
+ renderChips(); render(); bar();
});
}
function renderChips(){
@@ -91,11 +112,14 @@ function render(){
const grid=$('grid'); grid.innerHTML='';
g.forEach(x=>{
const d=document.createElement('div'); d.className='card';
- d.innerHTML=`<div class="top"><span class="emoji">${x.emoji}</span><span class="cat">${x.catLabel}</span></div>
+ const on = SEL.has(x.file);
+ d.innerHTML=`<div class="top"><label class="pick"><input type="checkbox" ${on?'checked':''}> pick</label><span class="emoji">${x.emoji}</span><span class="cat">${x.catLabel}</span></div>
<div class="note">${esc(x.note)}</div>
<div class="when">🕓 ${fmtWhen(x.created)}</div>
<div class="file">${esc(x.file)}</div>
<button>read the grown-up version →</button>`;
+ if(on) d.classList.add('sel');
+ d.querySelector('input').onchange=e=>{ e.target.checked?SEL.add(x.file):SEL.delete(x.file); d.classList.toggle('sel',e.target.checked); saveSel(); bar(); };
d.querySelector('button').onclick=()=>openMemo(x.file,x.title);
grid.appendChild(d);
});
diff --git a/selected.json b/selected.json
new file mode 100644
index 0000000..b5fb184
--- /dev/null
+++ b/selected.json
@@ -0,0 +1,5 @@
+{
+ "savedAt": "2026-08-18T20:18:26.694Z",
+ "count": 0,
+ "files": []
+}
\ No newline at end of file
diff --git a/server.js b/server.js
index 66d0272..54c1d4e 100644
--- a/server.js
+++ b/server.js
@@ -54,8 +54,39 @@ function cleanTitle(firstLine, fname) {
for (const [k, v] of Object.entries(swap)) t = t.replace(new RegExp('\\b' + k + '\\w*', 'gi'), v);
return t.charAt(0).toUpperCase() + t.slice(1);
}
-function kidNote(cat, title) {
- return `This is about: ${title}. ${cat.yes}`;
+const JARGON = { 'reconcile':'match up','remediation':'cleanup','canary':'watcher','metafield':'label',
+ 'orphan':'lost','backfill':'fill in','onboard':'add','scrape':'collect','reprice':'change the prices of',
+ 'redirect':'forward','provision':'set up','miswire':'wrong wiring','dedup':'remove doubles',
+ 'deploy':'put online','rsync':'copy up','pm2':'the site','nginx':'the web server','sku':'product code',
+ 'catalog':'product list','variant':'version','storefront':'store','gtag':'visitor counter',
+ 'ga4':'visitor counter','shopify':'the store','kamatera':'the server','dns':'web address',
+ 'private-label':'our own brand name','leak':'a hidden name showing','activation':'turning on',
+ 'gated':'needs your OK','wholesale':'our cost','retail':'the price shoppers pay' };
+function simplify(s) {
+ for (const [k, v] of Object.entries(JARGON)) s = s.replace(new RegExp('\\b' + k + '\\w*', 'gi'), v);
+ return s;
+}
+function extractSummary(body) {
+ const lines = body.split('\n');
+ for (let raw of lines) {
+ const l = raw.trim();
+ if (!l) continue;
+ if (/^#{1,6}\s/.test(l)) continue; // heading
+ if (/^[>|\-*]|^```|^\||^\d+\.\s/.test(l)) continue; // quote/table/list/code
+ if (/^\*\*(date|ticket|agent|status|cost|store|api|mode|owner|by|drafted|supersedes|context)\b/i.test(l)) continue;
+ const prose = l.replace(/\*\*/g, '').replace(/[`*_#>]/g, '').trim();
+ if (prose.length >= 40 && /[a-z]/i.test(prose[0])) {
+ let s = prose.split(/(?<=[.!?])\s/)[0]; // first sentence
+ if (s.length > 170) s = s.slice(0, 167) + '…';
+ return simplify(s);
+ }
+ }
+ return null;
+}
+function kidNote(cat, title, body) {
+ const sum = extractSummary(body);
+ const about = sum || simplify(title);
+ return `This is about: ${about} ${cat.yes}`;
}
function listGates() {
@@ -72,7 +103,7 @@ function listGates() {
const title = cleanTitle(first, f);
return {
file: f, title, category: cat.key, catLabel: cat.label, emoji: cat.emoji,
- note: kidNote(cat, title), created: st.mtimeMs, size: st.size,
+ note: kidNote(cat, title, body), created: st.mtimeMs, size: st.size,
};
}).sort((a, b) => b.created - a.created);
}
@@ -91,6 +122,16 @@ const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'application/json' });
return res.end(JSON.stringify({ total: gates.length, counts, gates }));
}
+ if (req.url === '/api/select' && req.method === 'POST') {
+ let b = ''; req.on('data', c => b += c); req.on('end', () => {
+ let files = [];
+ try { files = JSON.parse(b).files || []; } catch {}
+ const out = { savedAt: new Date().toISOString(), count: files.length, files };
+ fs.writeFileSync(path.join(__dirname, 'selected.json'), JSON.stringify(out, null, 2));
+ res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ ok: true, count: files.length }));
+ });
+ return;
+ }
if (req.url.startsWith('/api/memo')) {
const f = decodeURIComponent((req.url.split('?f=')[1] || ''));
const p = path.join(DIR, path.basename(f));
← 8fe25e6 strip 8-digit date stamps from titles
·
back to Gated Queue Runner
·
quality-gate note extraction (prose only, else title) bf673da →