← back to Wallco Ai
render 5400px/150-DPI TIF archives for 25 HOT geom tiles (from svg masters, 2-ink LZW); tif_path/bytes/dims recorded; archive-svg-5400.js + svg_raster_snap tif support
e5f92ceba29f563c5264e173ffbc1c8bbddd47fd · 2026-05-27 17:08:56 -0700 · Steve Abrams
Files touched
M data/auto-decide-ledger.jsonlM public/admin/drunk-curator.htmlA public/admin/rooms.htmlA scripts/archive-svg-5400.jsM scripts/svg_raster_snap.pyM server.js
Diff
commit e5f92ceba29f563c5264e173ffbc1c8bbddd47fd
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed May 27 17:08:56 2026 -0700
render 5400px/150-DPI TIF archives for 25 HOT geom tiles (from svg masters, 2-ink LZW); tif_path/bytes/dims recorded; archive-svg-5400.js + svg_raster_snap tif support
---
data/auto-decide-ledger.jsonl | 99 +++++++++++++++++++++++++++++++++++++++++
public/admin/drunk-curator.html | 2 +
public/admin/rooms.html | 71 +++++++++++++++++++++++++++++
scripts/archive-svg-5400.js | 52 ++++++++++++++++++++++
scripts/svg_raster_snap.py | 6 ++-
server.js | 19 ++++++++
6 files changed, 248 insertions(+), 1 deletion(-)
diff --git a/data/auto-decide-ledger.jsonl b/data/auto-decide-ledger.jsonl
index 1e761af..ba9532b 100644
--- a/data/auto-decide-ledger.jsonl
+++ b/data/auto-decide-ledger.jsonl
@@ -3933,3 +3933,102 @@
{"t":"2026-05-27T23:54:37.138Z","id":51847,"verdict":"hot_candidate","n":4,"see":"There are multiple giraffe motifs in a repeating pattern."}
{"t":"2026-05-27T23:54:39.289Z","id":51848,"verdict":"hot_candidate","n":4,"see":"This is a wallpaper tile with a repeating giraffe motif."}
{"t":"2026-05-27T23:54:41.722Z","id":51892,"verdict":"hot_candidate","n":15,"see":"A wallpaper tile with a repeating elephant motif."}
+{"t":"2026-05-27T23:54:43.912Z","id":51921,"verdict":"hot_candidate","n":6,"see":"A wallpaper tile with a repeating giraffe motif."}
+{"t":"2026-05-27T23:54:46.156Z","id":52089,"verdict":"hot_candidate","n":6,"see":"The tile shows a repeating pattern of giraffes and trees."}
+{"t":"2026-05-27T23:54:48.604Z","id":52090,"verdict":"hot_candidate","n":6,"see":"This is a wallpaper tile with a repeating giraffe and tree motif."}
+{"t":"2026-05-27T23:54:50.839Z","id":52096,"verdict":"hot_candidate","n":4,"see":"This is a wallpaper tile with a repeating giraffe motif."}
+{"t":"2026-05-27T23:54:53.186Z","id":52153,"verdict":"hot_candidate","n":4,"see":"This is a wallpaper tile with a repeating giraffe motif."}
+{"t":"2026-05-27T23:54:55.453Z","id":52188,"verdict":"hot_candidate","n":4,"see":"A repeating pattern of frogs and monkeys."}
+{"t":"2026-05-27T23:54:57.735Z","id":52189,"verdict":"hot_candidate","n":8,"see":"A repeating pattern of frogs and monkeys."}
+{"t":"2026-05-27T23:54:59.926Z","id":52454,"verdict":"hot_candidate","n":7,"see":"This is a wallpaper tile with a repeating giraffe motif."}
+{"t":"2026-05-27T23:55:02.491Z","id":52590,"verdict":"hot_candidate","n":4,"see":"This is a wallpaper tile with a repeating giraffe motif."}
+{"t":"2026-05-27T23:55:04.565Z","id":52591,"verdict":"hot_candidate","n":5,"see":"A repeating pattern of giraffes."}
+{"t":"2026-05-27T23:55:06.513Z","id":52592,"verdict":"hot_candidate","n":5,"see":"This is a wallpaper tile with a repeating giraffe motif."}
+{"t":"2026-05-27T23:55:08.664Z","id":52599,"verdict":"hot_candidate","n":4,"see":"A repeating pattern of lemurs with cocktails."}
+{"t":"2026-05-27T23:55:10.697Z","id":52600,"verdict":"hot_candidate","n":4,"see":"I see a repeating pattern of lemurs with cocktails."}
+{"t":"2026-05-27T23:55:13.109Z","id":52665,"verdict":"hot_candidate","n":4,"see":"A wallpaper tile with a repeating pattern of monkeys and champagne bottles."}
+{"t":"2026-05-27T23:55:15.843Z","id":52676,"verdict":"hot_candidate","n":4,"see":"A repeating pattern of monkeys and champagne bottles."}
+{"t":"2026-05-27T23:55:18.172Z","id":52730,"verdict":"hot_candidate","n":5,"see":"This is a wallpaper tile with a repeating giraffe motif."}
+{"t":"2026-05-27T23:55:20.482Z","id":52805,"verdict":"hot_candidate","n":4,"see":"A wallpaper tile with a repeating monkey motif."}
+{"t":"2026-05-27T23:55:22.571Z","id":52976,"verdict":"hot_candidate","n":5,"see":"A wallpaper tile with a repeating giraffe and tree motif."}
+{"t":"2026-05-27T23:55:24.712Z","id":52977,"verdict":"hot_candidate","n":5,"see":"Giraffe and tree motif repeats across the tile."}
+{"t":"2026-05-27T23:55:26.815Z","id":52978,"verdict":"hot_candidate","n":6,"see":"A repeating pattern of giraffes and trees."}
+{"t":"2026-05-27T23:55:29.626Z","id":53000,"verdict":"hot_candidate","n":4,"see":"I see a repeating pattern of gorillas holding champagne."}
+{"t":"2026-05-27T23:55:31.815Z","id":53020,"verdict":"hot_candidate","n":4,"see":"Four giraffe motifs create a repeating pattern."}
+{"t":"2026-05-27T23:55:33.948Z","id":53037,"verdict":"hot_candidate","n":4,"see":"Four giraffe motifs create a repeating pattern."}
+{"t":"2026-05-27T23:55:36.138Z","id":53038,"verdict":"hot_candidate","n":4,"see":"Four giraffe motifs are arranged in a repeating pattern."}
+{"t":"2026-05-27T23:55:38.271Z","id":53094,"verdict":"hot_candidate","n":4,"see":"This is a wallpaper tile with a repeating pattern of monkeys and champagne bottles."}
+{"t":"2026-05-27T23:55:40.637Z","id":53095,"verdict":"hot_candidate","n":4,"see":"A wallpaper tile with a repeating pattern of monkeys and champagne bottles."}
+{"t":"2026-05-27T23:55:42.963Z","id":53096,"verdict":"hot_candidate","n":4,"see":"A repeating pattern of monkeys with champagne bottles."}
+{"t":"2026-05-27T23:55:46.134Z","id":53193,"verdict":"hot_candidate","n":4,"see":"A repeating floral pattern with pink and white flowers."}
+{"t":"2026-05-27T23:55:48.870Z","id":53254,"verdict":"hot_candidate","n":4,"see":"This is a wallpaper tile with a repeating pattern of yellow cats and red flowers."}
+{"t":"2026-05-27T23:55:51.250Z","id":53255,"verdict":"hot_candidate","n":9,"see":"A wallpaper tile with a repeating damask pattern."}
+{"t":"2026-05-27T23:55:54.235Z","id":53382,"verdict":"hot_candidate","n":10,"see":"A repeating pattern of flowers in vases and glasses."}
+{"t":"2026-05-27T23:55:56.967Z","id":53383,"verdict":"hot_candidate","n":4,"see":"A repeating pattern of owls and cocktails."}
+{"t":"2026-05-27T23:55:59.765Z","id":53384,"verdict":"not","reason":"centered-hero (n=2)","see":"There is a centered monkey motif with other motifs around it."}
+{"t":"2026-05-27T23:56:02.673Z","id":53385,"verdict":"not","reason":"centered-hero (n=3)","see":"A centered owl motif with partial owls in the corners."}
+{"t":"2026-05-27T23:56:05.706Z","id":53386,"verdict":"not","reason":"centered-hero (n=2)","see":"A centered owl motif with partial owls in the corners."}
+{"t":"2026-05-27T23:56:08.496Z","id":53387,"verdict":"hot_candidate","n":4,"see":"A repeating pattern of owls, bottles, and glasses."}
+{"t":"2026-05-27T23:56:11.208Z","id":53388,"verdict":"hot_candidate","n":4,"see":"This is a wallpaper repeat of owls and wine glasses."}
+{"t":"2026-05-27T23:56:14.043Z","id":53389,"verdict":"hot_candidate","n":4,"see":"A repeating pattern of peacocks and bottles."}
+{"t":"2026-05-27T23:56:16.734Z","id":53390,"verdict":"not","reason":"centered-hero (n=3)","see":"A centered orangutan motif with partial motifs around the edges."}
+{"t":"2026-05-27T23:56:19.552Z","id":53391,"verdict":"not","reason":"centered-hero (n=4)","see":"A centered owl motif with partial owls in the corners."}
+{"t":"2026-05-27T23:56:22.455Z","id":53392,"verdict":"hot_candidate","n":4,"see":"A repeating pattern of monkeys holding hearts."}
+{"t":"2026-05-27T23:56:25.207Z","id":53408,"verdict":"hot_candidate","n":4,"see":"A wallpaper tile with a repeating beer bottle and parrot motif."}
+{"t":"2026-05-27T23:56:27.422Z","id":53658,"verdict":"not","reason":"centered-hero (n=1)","see":"A single orangutan is centered in the image."}
+{"t":"2026-05-27T23:56:29.687Z","id":53676,"verdict":"hot_candidate","n":7,"see":"The tile shows a repeating pattern of food and drink motifs."}
+{"t":"2026-05-27T23:56:31.837Z","id":53679,"verdict":"not","reason":"too-few-instances (n=2<3)","see":"There are two instances of a wine glass motif with a large empty area."}
+{"t":"2026-05-27T23:56:33.795Z","id":53686,"verdict":"hot_candidate","n":6,"see":"A repeating pattern of green teapots and plants on a beige background."}
+{"t":"2026-05-27T23:56:36.136Z","id":53689,"verdict":"not","reason":"large-empty-void","see":"The image shows a repeating pattern of wine glasses with various objects inside, with a large empty area."}
+{"t":"2026-05-27T23:56:38.290Z","id":53692,"verdict":"not","reason":"too-few-instances (n=2<3)","see":"Two bird motifs are visible with a large empty area."}
+{"t":"2026-05-27T23:56:40.665Z","id":53693,"verdict":"not","reason":"too-few-instances (n=2<3)","see":"I see two instances of a motif with a large empty area."}
+{"t":"2026-05-27T23:56:43.013Z","id":53694,"verdict":"not","reason":"centered-hero (n=1)","see":"A wine bottle with an elephant label is centered on a large empty background."}
+{"t":"2026-05-27T23:56:45.372Z","id":53695,"verdict":"hot_candidate","n":3,"see":"I see a wallpaper tile with a repeating fruit motif."}
+{"t":"2026-05-27T23:56:47.605Z","id":53696,"verdict":"not","reason":"large-empty-void","see":"I see three partial motifs with a large empty area."}
+{"t":"2026-05-27T23:56:49.888Z","id":53697,"verdict":"not","reason":"large-empty-void","see":"There are three instances of a dark green motif on a light yellow background."}
+{"t":"2026-05-27T23:56:52.114Z","id":53698,"verdict":"not","reason":"centered-hero (n=1)","see":"A large central motif with a lot of empty background."}
+{"t":"2026-05-27T23:56:54.340Z","id":53699,"verdict":"not","reason":"too-few-instances (n=2<3)","see":"There are two instances of the motif with a large empty area."}
+{"t":"2026-05-27T23:56:56.518Z","id":53700,"verdict":"not","reason":"centered-hero (n=1)","see":"A single large motif of a panda holding a bowl of fruit is centered on a dark background."}
+{"t":"2026-05-27T23:56:58.760Z","id":53701,"verdict":"not","reason":"centered-hero (n=1)","see":"A single large face is centered in the tile."}
+{"t":"2026-05-27T23:57:01.233Z","id":53702,"verdict":"not","reason":"large-empty-void","see":"The tile features a repeating pattern of elephants holding drinks, with a large empty area."}
+{"t":"2026-05-27T23:57:03.581Z","id":53704,"verdict":"hot_candidate","n":4,"see":"A repeating pattern of wine glasses and frogs."}
+{"t":"2026-05-27T23:57:05.892Z","id":53705,"verdict":"not","reason":"large-empty-void","see":"There are four instances of the motif with a large empty area."}
+{"t":"2026-05-27T23:57:08.311Z","id":53706,"verdict":"not","reason":"too-few-instances (n=2<3)","see":"I see two instances of the dominant motif with a large empty area."}
+{"t":"2026-05-27T23:57:10.439Z","id":53707,"verdict":"hot_candidate","n":6,"see":"A wallpaper tile with a repeating pattern of capybaras."}
+{"t":"2026-05-27T23:57:12.591Z","id":53708,"verdict":"not","reason":"too-few-instances (n=2<3)","see":"I see two cocktail glasses with a large empty area."}
+{"t":"2026-05-27T23:57:14.850Z","id":53709,"verdict":"not","reason":"centered-hero (n=3)","see":"A centered bottle with partial bottles on the sides and top."}
+{"t":"2026-05-27T23:57:16.976Z","id":53710,"verdict":"hot_candidate","n":5,"see":"A repeating pattern of various vases and pitchers."}
+{"t":"2026-05-27T23:57:19.318Z","id":53711,"verdict":"hot_candidate","n":6,"see":"A wallpaper tile with a repeating pattern of various drinkware and containers."}
+{"t":"2026-05-27T23:57:21.622Z","id":53713,"verdict":"hot_candidate","n":6,"see":"A repeating pattern of various objects."}
+{"t":"2026-05-27T23:57:23.829Z","id":53714,"verdict":"hot_candidate","n":6,"see":"A repeating pattern of dark green floral motifs on a tan background."}
+{"t":"2026-05-27T23:57:26.176Z","id":53715,"verdict":"hot_candidate","n":7,"see":"A wallpaper tile with a repeating pattern of vases and teapots."}
+{"t":"2026-05-27T23:57:28.450Z","id":53717,"verdict":"hot_candidate","n":3,"see":"The tile features a repeating pattern of vases and a figure in a bowl."}
+{"t":"2026-05-27T23:57:30.766Z","id":53718,"verdict":"hot_candidate","n":6,"see":"A repeating pattern of vases and other objects."}
+{"t":"2026-05-27T23:57:33.089Z","id":53719,"verdict":"hot_candidate","n":12,"see":"A repeating pattern of stylized figures with flowers and drinks."}
+{"t":"2026-05-27T23:57:35.135Z","id":53720,"verdict":"hot_candidate","n":4,"see":"A repeating pattern of vases and floral elements."}
+{"t":"2026-05-27T23:57:37.147Z","id":53721,"verdict":"not","reason":"centered-hero (n=1)","see":"A single wine glass is centered with a large empty area."}
+{"t":"2026-05-27T23:57:39.349Z","id":53722,"verdict":"not","reason":"large-empty-void","see":"I see a wallpaper tile with a large empty area and three instances of the dominant motif."}
+{"t":"2026-05-27T23:57:41.739Z","id":53723,"verdict":"hot_candidate","n":7,"see":"A repeating pattern of tribal faces and figures."}
+{"t":"2026-05-27T23:57:43.989Z","id":53724,"verdict":"hot_candidate","n":7,"see":"A wallpaper tile with a repeating pattern of frogs and glasses."}
+{"t":"2026-05-27T23:57:46.044Z","id":53725,"verdict":"hot_candidate","n":5,"see":"A repeating pattern of stylized plants and vertical lines."}
+{"t":"2026-05-27T23:57:48.260Z","id":53726,"verdict":"hot_candidate","n":10,"see":"A wallpaper tile with a repeating pattern of various objects."}
+{"t":"2026-05-27T23:57:50.451Z","id":53727,"verdict":"not","reason":"too-few-instances (n=2<3)","see":"Two large teapot motifs on a light background."}
+{"t":"2026-05-27T23:57:52.477Z","id":53728,"verdict":"hot_candidate","n":4,"see":"There are multiple instances of cups and chalices."}
+{"t":"2026-05-27T23:57:54.973Z","id":53730,"verdict":"hot_candidate","n":8,"see":"A repeating pattern of various antique objects."}
+{"t":"2026-05-27T23:57:57.074Z","id":53732,"verdict":"hot_candidate","n":6,"see":"This is a wallpaper repeat with a floral and abstract motif."}
+{"t":"2026-05-27T23:57:59.297Z","id":53733,"verdict":"hot_candidate","n":5,"see":"A repeating pattern of wine glasses and floral arrangements."}
+{"t":"2026-05-27T23:58:01.675Z","id":53734,"verdict":"not","reason":"large-empty-void","see":"I see four instances of a motif with a large empty area in the center."}
+{"t":"2026-05-27T23:58:03.772Z","id":53736,"verdict":"not","reason":"centered-hero (n=2)","see":"There is a large empty area with a centered motif."}
+{"t":"2026-05-27T23:58:06.095Z","id":53743,"verdict":"not","reason":"large-empty-void","see":"I see a wallpaper tile with a giraffe motif and a large empty area."}
+{"t":"2026-05-27T23:58:08.157Z","id":53746,"verdict":"not","reason":"centered-hero (n=3)","see":"A centered monster motif with partial motifs at the edges."}
+{"t":"2026-05-27T23:58:10.398Z","id":53749,"verdict":"hot_candidate","n":7,"see":"A repeating pattern of cocktail glasses and tropical plants."}
+{"t":"2026-05-27T23:58:13.131Z","id":53754,"verdict":"not","reason":"centered-hero (n=1)","see":"A centered monkey with a large empty area."}
+{"t":"2026-05-27T23:58:15.225Z","id":53757,"verdict":"hot_candidate","n":4,"see":"I see a wallpaper repeat of teacups and other objects."}
+{"t":"2026-05-27T23:58:17.484Z","id":53759,"verdict":"hot_candidate","n":4,"see":"I see a repeating pattern of a deer in a pot, a bird, a wine glass, and a bug."}
+{"t":"2026-05-27T23:58:19.693Z","id":53761,"verdict":"hot_candidate","n":15,"see":"A wallpaper tile with a repeating pattern of white floral motifs on a brown background."}
+{"t":"2026-05-27T23:58:21.758Z","id":53762,"verdict":"not","reason":"centered-hero (n=3)","see":"A centered motif with large empty areas."}
+{"t":"2026-05-27T23:58:24.001Z","id":53763,"verdict":"hot_candidate","n":6,"see":"This is a wallpaper tile with a repeating floral pattern."}
+{"t":"2026-05-27T23:58:26.279Z","id":53789,"verdict":"hot_candidate","n":6,"see":"This is a wallpaper tile with a repeating pattern of jungle motifs."}
+{"t":"2026-05-27T23:58:28.553Z","id":53792,"verdict":"not","reason":"large-empty-void","see":"There are three instances of a bird and floral motif with a large empty area."}
+{"t":"2026-05-27T23:58:30.595Z","id":53813,"verdict":"hot_candidate","n":4,"see":"A repeating pattern of animal motifs."}
+{"t":"2026-05-27T23:58:32.942Z","id":53814,"verdict":"hot_candidate","n":12,"see":"A repeating pattern of various vases and pots."}
diff --git a/public/admin/drunk-curator.html b/public/admin/drunk-curator.html
index cd6845a..a9c69c6 100644
--- a/public/admin/drunk-curator.html
+++ b/public/admin/drunk-curator.html
@@ -103,6 +103,7 @@
<input type="checkbox" id="autotoggle"> 🔍 Auto-ID <span style="color:#6a737d">(Gemini ~$.003/item)</span>
</label>
<button id="fsbtn" title="toggle fullscreen" onclick="toggleFS()" style="background:var(--panel);color:var(--ink);border:1px solid var(--line);border-radius:7px;padding:5px 10px;font-size:13px;cursor:pointer">⛶ Fullscreen</button>
+ <a id="gridlink" title="grid view — drag to bulk-select + apply" style="background:var(--panel);color:var(--ink);border:1px solid var(--line);border-radius:7px;padding:5px 10px;font-size:13px;cursor:pointer;text-decoration:none">▦ Grid (bulk)</a>
<div class="stats">
<span>seen <b id="seen">0</b></span><span>🔥 hot <b id="hot">0</b></span>
<span>👎 not <b id="not">0</b></span><span class="streak" id="streak"></span>
@@ -521,6 +522,7 @@ _autoTog.addEventListener('change',()=>{
if(autoIdOn){ autoIdCurrent(); }
else { const b=$('#eboxes'); if(b) b.innerHTML=''; document.getElementById('scanning')?.remove(); }
});
+{ const gl=document.getElementById('gridlink'); if(gl) gl.href=q('/admin/cactus-curator'+(CATEGORY?'?category='+encodeURIComponent(CATEGORY):'')); }
loadCollections();
(async function(){ const r=await fetch(q('/api/admin/drunk/list'+(CATEGORY?'?category='+encodeURIComponent(CATEGORY):''))); QUEUE=(await r.json()).items||[]; render(); })();
</script>
diff --git a/public/admin/rooms.html b/public/admin/rooms.html
new file mode 100644
index 0000000..9b0f04c
--- /dev/null
+++ b/public/admin/rooms.html
@@ -0,0 +1,71 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Room Renders — wallco.ai admin</title>
+<style>
+ :root{--cols:4;--bg:#0e0f10;--panel:#17191b;--line:#272b2e;--ink:#e9ecef;--mut:#9aa3ab;--accent:#7bc96f;}
+ *{box-sizing:border-box;}
+ body{margin:0;background:var(--bg);color:var(--ink);font:14px/1.45 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;}
+ header{position:sticky;top:0;z-index:20;background:rgba(14,15,16,.96);backdrop-filter:blur(8px);border-bottom:1px solid var(--line);padding:10px 16px;display:flex;gap:14px;align-items:center;flex-wrap:wrap;}
+ h1{font-size:16px;margin:0;font-weight:650;} h1 .leaf{color:var(--accent);}
+ .pill{background:var(--panel);border:1px solid var(--line);border-radius:999px;padding:3px 10px;font-size:12px;color:var(--mut);} .pill b{color:var(--ink);}
+ label.ctl{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--mut);}
+ select,input[type=range]{accent-color:var(--accent);} select{background:var(--panel);color:var(--ink);border:1px solid var(--line);border-radius:7px;padding:5px 8px;font-size:13px;}
+ .grid{display:grid;grid-template-columns:repeat(var(--cols),1fr);gap:12px;padding:14px 16px 80px;}
+ .card{background:var(--panel);border:1px solid var(--line);border-radius:10px;overflow:hidden;display:flex;flex-direction:column;}
+ .card a{display:block;aspect-ratio:1/1;background:#000;}
+ .card img{width:100%;height:100%;object-fit:cover;display:block;}
+ .meta{padding:7px 9px;font-size:12px;display:flex;justify-content:space-between;align-items:center;}
+ .rt{color:var(--ink);text-transform:capitalize;font-weight:600;}
+ .when{color:var(--mut);font-size:10.5px;font-variant-numeric:tabular-nums;}
+ .did{color:var(--mut);font-size:11px;}
+ #empty,#err{padding:60px 16px;text-align:center;color:var(--mut);} #err{color:#ff9a9a;}
+</style>
+</head>
+<body>
+<header>
+ <h1><span class="leaf">🛋</span> Room Renders</h1>
+ <span class="pill" id="stat">loading…</span>
+ <label class="ctl">Room
+ <select id="filter"><option value="">all types</option></select>
+ </label>
+ <label class="ctl">Density <input type="range" id="density" min="2" max="8" step="1" value="4"></label>
+</header>
+<div id="err" style="display:none"></div>
+<div id="empty" style="display:none">No room renders yet — hit 🪑 Furniture in the curator.</div>
+<div class="grid" id="grid"></div>
+<script>
+const ADMIN=new URLSearchParams(location.search).get('admin');
+const q=s=>ADMIN?(s+(s.includes('?')?'&':'?')+'admin='+encodeURIComponent(ADMIN)):s;
+const $=id=>document.getElementById(id);
+let ALL=[];
+$('density').value=+(localStorage.getItem('rooms_density')||4); document.documentElement.style.setProperty('--cols',$('density').value);
+$('density').addEventListener('input',e=>{localStorage.setItem('rooms_density',e.target.value);document.documentElement.style.setProperty('--cols',e.target.value);});
+$('filter').addEventListener('change',render);
+function fmt(iso){ if(!iso)return''; const d=new Date(iso); return isNaN(d)?iso:d.toLocaleString(undefined,{month:'short',day:'numeric',hour:'numeric',minute:'2-digit'}); }
+function render(){
+ const f=$('filter').value;
+ const list=ALL.filter(r=>!f||r.room_type===f);
+ $('empty').style.display=list.length?'none':'block';
+ $('grid').innerHTML=list.map(r=>`
+ <div class="card">
+ <a href="${q('/design/'+r.design_id)}" target="_blank" title="open design #${r.design_id}"><img loading="lazy" src="${r.image_path}" alt="room ${r.id}"></a>
+ <div class="meta"><span class="rt">${(r.room_type||'').replace(/_/g,' ')}</span><span class="did">#${r.design_id}</span></div>
+ <div class="meta" style="padding-top:0"><span class="when">🕓 ${fmt(r.created_at)}</span></div>
+ </div>`).join('');
+ $('stat').innerHTML=`<b>${ALL.length}</b> renders · <b>${list.length}</b> shown`;
+}
+(async function(){
+ try{
+ const r=await fetch(q('/api/admin/rooms/list')); if(!r.ok) throw new Error('list '+r.status+(r.status===404?' — append ?admin=TOKEN':''));
+ ALL=(await r.json()).items||[];
+ const types=[...new Set(ALL.map(x=>x.room_type).filter(Boolean))].sort();
+ for(const t of types){ const o=document.createElement('option'); o.value=t; o.textContent=t.replace(/_/g,' '); $('filter').appendChild(o); }
+ render();
+ }catch(e){ $('err').style.display='block'; $('err').textContent='Failed: '+e.message; }
+})();
+</script>
+</body>
+</html>
diff --git a/scripts/archive-svg-5400.js b/scripts/archive-svg-5400.js
new file mode 100644
index 0000000..78c8b83
--- /dev/null
+++ b/scripts/archive-svg-5400.js
@@ -0,0 +1,52 @@
+#!/usr/bin/env node
+/**
+ * Render full-size 150-DPI archives (5400×5400 = 36" tile) for HOT geometric SVG
+ * tessellations, straight from each tile's .svg master (vector → crisp, no LANCZOS).
+ * Saves a 2-ink LZW-compressed TIF to data/tif/ and records tif_path/bytes/dims.
+ * Targets every svg-tessellate tile that was HOT'd (published OR curator-loved).
+ *
+ * Usage: node scripts/archive-svg-5400.js [--size 5400] [--ids 1,2,3]
+ */
+const fs = require('fs');
+const path = require('path');
+const { spawnSync } = require('child_process');
+
+const ROOT = path.join(__dirname, '..');
+const TIF_DIR = path.join(ROOT, 'data', 'tif');
+const VENV_PY = path.join(ROOT, 'scripts', 'vectorize', '.venv', 'bin', 'python3');
+const SNAP = path.join(ROOT, 'scripts', 'svg_raster_snap.py');
+const DB = process.env.WALLCO_DB || 'dw_unified';
+const flag = (k, d) => { const m = process.argv.find(a => a.startsWith('--' + k + '=')); return m ? m.split('=')[1] : d; };
+const SIZE = parseInt(flag('size', '5400'), 10);
+const IDS = flag('ids', '');
+
+fs.mkdirSync(TIF_DIR, { recursive: true });
+const psql = sql => {
+ const r = spawnSync('psql', ['-d', DB, '-tAc', sql], { encoding: 'utf8' });
+ if (r.status !== 0) throw new Error('psql: ' + (r.stderr || '').slice(0, 300));
+ return (r.stdout || '').trim();
+};
+
+const where = IDS
+ ? `id IN (${IDS.split(',').map(n => parseInt(n, 10)).filter(Number.isFinite).join(',')})`
+ : `generator='svg-tessellate' AND (is_published=TRUE OR 'curator-loved'=ANY(tags)) AND tif_path IS NULL`;
+const rows = psql(
+ `SELECT id || '|' || local_path || '|' || COALESCE(palette->0->>'hex','#16203c') || '|' || COALESCE(palette->1->>'hex','#f2e9d3') ` +
+ `FROM all_designs WHERE ${where} ORDER BY id;`
+).split('\n').filter(Boolean);
+
+console.log(`Archiving ${rows.length} HOT tiles → ${SIZE}px TIF\n`);
+let ok = 0;
+for (const row of rows) {
+ const [id, png, ink, paper] = row.split('|');
+ const svg = png.replace(/\.png$/, '.svg');
+ if (!fs.existsSync(svg)) { console.log(` ✗ #${id}: no svg master (${path.basename(svg)})`); continue; }
+ const tif = path.join(TIF_DIR, path.basename(png).replace(/\.png$/, `_${SIZE}.tif`));
+ const r = spawnSync(VENV_PY, [SNAP, svg, tif, String(SIZE), ink, paper], { encoding: 'utf8' });
+ if (r.status !== 0 || !fs.existsSync(tif)) { console.log(` ✗ #${id}: ${(r.stderr || r.stdout || 'raster failed').slice(0, 140)}`); continue; }
+ const bytes = fs.statSync(tif).size;
+ psql(`UPDATE all_designs SET tif_path='${tif.replace(/'/g, "''")}', tif_bytes=${bytes}, tif_w_px=${SIZE}, tif_h_px=${SIZE} WHERE id=${id};`);
+ ok++;
+ console.log(` ✓ #${id} ${(bytes/1024/1024).toFixed(2)} MB ${path.basename(tif)}`);
+}
+console.log(`\nDone: ${ok}/${rows.length} archives rendered → ${TIF_DIR}`);
diff --git a/scripts/svg_raster_snap.py b/scripts/svg_raster_snap.py
index 752eabc..187a3f5 100644
--- a/scripts/svg_raster_snap.py
+++ b/scripts/svg_raster_snap.py
@@ -26,6 +26,10 @@ dn = ((a - navy) ** 2).sum(2)
dc = ((a - cream) ** 2).sum(2)
mask = (dn <= dc)[..., None]
out = np.where(mask, navy, cream).astype(np.uint8)
-Image.fromarray(out, 'RGB').save(png, optimize=True)
+im2 = Image.fromarray(out, 'RGB')
+if png.lower().endswith(('.tif', '.tiff')):
+ im2.save(png, compression='tiff_lzw') # full-size archive — lossless, small for 2 inks
+else:
+ im2.save(png, optimize=True)
navy_pct = round(float(mask.mean()) * 100, 1)
print(f"OK {png} {navy_pct}")
diff --git a/server.js b/server.js
index 65ca2db..11f916c 100644
--- a/server.js
+++ b/server.js
@@ -1124,6 +1124,25 @@ app.get('/admin/cactus-curator', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'admin', 'cactus-curator.html'));
});
+// ── /admin/rooms — master gallery of EVERY saved room-setting render ───────
+app.get('/admin/rooms', (req, res) => {
+ if (!isAdmin(req)) return res.status(404).type('html').send('<h1>404</h1>');
+ res.sendFile(path.join(__dirname, 'public', 'admin', 'rooms.html'));
+});
+app.get('/api/admin/rooms/list', (req, res) => {
+ if (!isAdmin(req)) return res.status(404).json({ error: 'not found' });
+ try {
+ const raw = psqlQuery(
+ "SELECT COALESCE(json_agg(t ORDER BY t.id DESC), '[]'::json) FROM (" +
+ "SELECT id, design_id, room_type, image_path, to_char(created_at,'YYYY-MM-DD HH24:MI') AS created_at " +
+ "FROM wallco_rooms WHERE image_path IS NOT NULL" +
+ ") t;"
+ );
+ const items = JSON.parse(raw || '[]');
+ res.json({ ok: true, count: items.length, items });
+ } catch (e) { res.status(500).json({ error: e.message }); }
+});
+
// ── /admin/defect-registry — read-only viewer over wallco_defect_registry ──
// "Keep all bad items in their own db so we never recreate the same error"
// (Steve 2026-05-27). Browse every catalogued defect with its image, defect
← f02d8c5 geom tessellation: add --palette colorways; generate terraco
·
back to Wallco Ai
·
clean-edge upscale (#53677): confirm potrace vectorize beats f460ec8 →