[object Object]

← back to Wallco Ai

curator 'Remove from all surfaces' now hard-deletes the TIF master (bulk + per-id bad paths) + NULLs tif_path; resolves Henry archive; confirm text updated

3b8020508a650311ca74c383e35e7499e091540a · 2026-06-02 08:05:08 -0700 · Steve Abrams

Files touched

Diff

commit 3b8020508a650311ca74c383e35e7499e091540a
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue Jun 2 08:05:08 2026 -0700

    curator 'Remove from all surfaces' now hard-deletes the TIF master (bulk + per-id bad paths) + NULLs tif_path; resolves Henry archive; confirm text updated
---
 public/admin/design-curator.html | 21 +++++++++++----
 server.js                        | 55 +++++++++++++++++++++++++++++++++++-----
 2 files changed, 65 insertions(+), 11 deletions(-)

diff --git a/public/admin/design-curator.html b/public/admin/design-curator.html
index 6955bc5..2bd7472 100644
--- a/public/admin/design-curator.html
+++ b/public/admin/design-curator.html
@@ -206,7 +206,7 @@
         <option value="seam">Seam quality ▼</option>
         <option value="newest">Newest</option>
         <option value="oldest">Oldest</option>
-        <option value="colorway">Colorway</option>
+        <option value="colorway">Color (hue) ▦</option>
       </select>
     </label>
     <label class="ctl">Show
@@ -374,14 +374,25 @@ $('hintall')?.addEventListener('click', () => { $('filter').value='all'; LS.f='a
 
 function num(v){ return v==null ? null : Number(v); }
 
+// dominant_hex → sort key: group by hue around the colour wheel; push near-greys
+// (low saturation) to the end ordered dark→light, so colour sort reads cleanly.
+function hueKey(hex){
+  const m = /^#?([0-9a-f]{6})$/i.exec(String(hex||'').trim());
+  if(!m) return 1000;
+  const n = parseInt(m[1],16), r=(n>>16&255)/255, g=(n>>8&255)/255, b=(n&255)/255;
+  const mx=Math.max(r,g,b), mn=Math.min(r,g,b), d=mx-mn, l=(mx+mn)/2;
+  const sat = d===0 ? 0 : d/(1-Math.abs(2*l-1));
+  let h=0; if(d){ if(mx===r)h=((g-b)/d+6)%6; else if(mx===g)h=(b-r)/d+2; else h=(r-g)/d+4; h*=60; }
+  return sat<0.12 ? 720 + (1-l)*100 : h;   // greys after all hues, dark→light
+}
 function sortFns(mode){
   const r = d => num(d.rank_score) ?? -1, v = d => num(d.vision_score) ?? -1, s = d => num(d.seam_score) ?? -1;
   switch(mode){
     case 'vision':   return (a,b)=> v(b)-v(a) || a.id-b.id;
     case 'seam':     return (a,b)=> s(b)-s(a) || a.id-b.id;
-    case 'newest':   return (a,b)=> new Date(b.created_at)-new Date(a.created_at);
-    case 'oldest':   return (a,b)=> new Date(a.created_at)-new Date(b.created_at);
-    case 'colorway': return (a,b)=> (a.category||'').localeCompare(b.category||'') || r(b)-r(a);
+    case 'newest':   return (a,b)=> new Date(b.created_at)-new Date(a.created_at) || b.id-a.id;
+    case 'oldest':   return (a,b)=> new Date(a.created_at)-new Date(b.created_at) || a.id-b.id;
+    case 'colorway': return (a,b)=> hueKey(a.dominant_hex)-hueKey(b.dominant_hex) || a.id-b.id;  // sort by COLOR (hue wheel)
     default:         return (a,b)=> r(b)-r(a) || a.id-b.id;   // composite rank
   }
 }
@@ -882,7 +893,7 @@ function escapeHtml(s){ return String(s||'').replace(/&/g,'&amp;').replace(/</g,
 async function bulk(action){
   const ids = [...selected];
   if(!ids.length) return;
-  if(action==='bad' && !confirm(`Remove ${ids.length} designs from all surfaces? (quarantines the PNGs + drops them from the live site)`)) return;
+  if(action==='bad' && !confirm(`Remove ${ids.length} designs from all surfaces? (quarantines the PNGs + drops them from the live site + HARD-DELETES the TIF masters — irreversible)`)) return;
   if(action==='unpublish' && !confirm(`Unpublish ${ids.length} designs from the live catalog? (sets is_published=FALSE; fully reversible via Publish — no quarantine, no tagging)`)) return;
 
   // ONE batched request — not N un-awaited decide() calls. The old loop fired a
diff --git a/server.js b/server.js
index c1f83ba..823e594 100644
--- a/server.js
+++ b/server.js
@@ -2711,14 +2711,46 @@ function patchDesignsSnapshot(ids) {
   return n;
 }
 
+// Hard-delete the full-size TIF master for a design marked bad. Steve 2026-06-02:
+// "Remove from all surfaces" must also kill the TIF, not just quarantine the PNG —
+// otherwise orphan ~2 GB-class masters pile up on the Henry archive every removal.
+// IRREVERSIBLE by design: the publish gate refuses NULL tif_path, so a deleted TIF
+// means that design can never be republished without regenerating the master
+// (intended — it's a "bad" verdict). `tif_path` in PG is a Mac2-absolute path that
+// may not exist as-is; on Mac2 the real archive lives at
+// /Volumes/Henry/wallco-ai-archive/tif/<id>.tif. Try every known location, rm the
+// first that resolves, return its path (or null if nothing was on disk).
+const TIF_ARCHIVE_DIRS = [
+  path.join(__dirname, 'data', 'tif'),
+  '/Volumes/Henry/wallco-ai-archive/tif',
+];
+function deleteTifForDesign(id, tifPath) {
+  const candidates = [];
+  if (tifPath && typeof tifPath === 'string') {
+    candidates.push(tifPath);
+    candidates.push(resolveAssetLocal(tifPath, 'tif'));
+    const base = path.basename(tifPath);
+    for (const d of TIF_ARCHIVE_DIRS) candidates.push(path.join(d, base));
+  }
+  for (const d of TIF_ARCHIVE_DIRS) candidates.push(path.join(d, id + '.tif'));
+  const seen = new Set();
+  for (const c of candidates) {
+    if (!c || seen.has(c)) continue;
+    seen.add(c);
+    try {
+      if (fs.existsSync(c) && fs.statSync(c).isFile()) { fs.unlinkSync(c); return c; }
+    } catch (e) { console.warn('[cactus bad tif delete] failed:', c, e.message); }
+  }
+  return null;
+}
+
 // Apply one cactus verdict. body { action: 'bad'|'digital'|'fix'|'live'|'unpublish' }.
 function applyCactusDecision(id, action, opts = {}) {
-  const raw = psqlQuery(`SELECT row_to_json(t) FROM (SELECT id, local_path FROM all_designs WHERE id=${id}) t;`);
+  const raw = psqlQuery(`SELECT row_to_json(t) FROM (SELECT id, local_path, tif_path FROM all_designs WHERE id=${id}) t;`);
   if (!raw) throw new Error('design not found');
   const row = JSON.parse(raw);
   if (action === 'bad') {
-    // 1) Bad Pattern — remove from all surfaces + quarantine the PNG.
-    psqlExecLocal(`UPDATE all_designs SET is_published=FALSE, user_removed=TRUE, web_viewer=FALSE WHERE id=${id};`);
+    // 1) Bad Pattern — remove from all surfaces + quarantine the PNG + HARD-DELETE the TIF.
     if (row.local_path && fs.existsSync(row.local_path)) {
       try {
         const qDir = path.join(__dirname, 'data', 'generated_cactus_quarantine');
@@ -2727,6 +2759,11 @@ function applyCactusDecision(id, action, opts = {}) {
         if (!fs.existsSync(dest)) fs.renameSync(row.local_path, dest);
       } catch (e) { console.warn('[cactus bad] quarantine move failed:', e.message); }
     }
+    // Hard-delete the TIF master and NULL the column so the row reflects reality.
+    let deletedTif = null;
+    try { deletedTif = deleteTifForDesign(id, row.tif_path); } catch (e) { console.warn('[cactus bad] tif delete:', e.message); }
+    if (deletedTif) console.log(`[cactus bad] deleted TIF for #${id}: ${deletedTif}`);
+    psqlExecLocal(`UPDATE all_designs SET is_published=FALSE, user_removed=TRUE, web_viewer=FALSE, tif_path=NULL WHERE id=${id};`);
     dropFromIndex([id]);   // SKU leaves the live public index immediately
   } else if (action === 'digital') {
     // 2) Unpublish, sell as digital file — stamp the date field.
@@ -2856,11 +2893,13 @@ app.post('/api/cactus-decision/bulk', express.json({ limit: '256kb' }), (req, re
   const idList = ids.join(',');
   try {
     if (action === 'bad') {
-      // Grab local_paths up front so we can quarantine the PNGs after the UPDATE.
-      const rawPaths = psqlQuery(`SELECT json_agg(t) FROM (SELECT id, local_path FROM all_designs WHERE id IN (${idList})) t;`);
-      psqlExecLocal(`UPDATE all_designs SET is_published=FALSE, user_removed=TRUE, web_viewer=FALSE WHERE id IN (${idList});`);
+      // Grab local_paths + tif_paths up front so we can quarantine the PNGs and
+      // HARD-DELETE the TIF masters after the UPDATE.
+      const rawPaths = psqlQuery(`SELECT json_agg(t) FROM (SELECT id, local_path, tif_path FROM all_designs WHERE id IN (${idList})) t;`);
+      psqlExecLocal(`UPDATE all_designs SET is_published=FALSE, user_removed=TRUE, web_viewer=FALSE, tif_path=NULL WHERE id IN (${idList});`);
       const rows = rawPaths && rawPaths.length > 2 ? (JSON.parse(rawPaths) || []) : [];
       const qDir = path.join(__dirname, 'data', 'generated_cactus_quarantine');
+      let tifsDeleted = 0;
       for (const row of rows) {
         if (row.local_path && fs.existsSync(row.local_path)) {
           try {
@@ -2869,7 +2908,11 @@ app.post('/api/cactus-decision/bulk', express.json({ limit: '256kb' }), (req, re
             if (!fs.existsSync(dest)) fs.renameSync(row.local_path, dest);
           } catch (e) { console.warn('[cactus bulk bad] quarantine move failed:', e.message); }
         }
+        // Hard-delete the full-size TIF master (IRREVERSIBLE — see deleteTifForDesign).
+        try { if (deleteTifForDesign(row.id, row.tif_path)) tifsDeleted++; }
+        catch (e) { console.warn('[cactus bulk bad] tif delete failed:', e.message); }
       }
+      if (tifsDeleted) console.log(`[cactus bulk bad] hard-deleted ${tifsDeleted} TIF master(s)`);
     } else if (action === 'digital') {
       psqlExecLocal(`UPDATE all_designs
         SET is_published=FALSE, web_viewer=FALSE, digital_file_at=now(),

← 709a34d Full-res TIF colorways + hi-res mural preview  ·  back to Wallco Ai  ·  audit: unpublish 17 dogs published with NULL tif_path (un-or 13bf818 →