← 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
M public/admin/design-curator.htmlM server.js
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,'&').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 →