← back to Dw Photo Capture
feat: sample-request sticker popup + scan cost throttle
7f00ab46cab93e41dc090930d831c2fb75865ab8 · 2026-07-06 17:16:32 -0700 · Steve Abrams
- #sampleModal popup (reuses .hidden modal pattern): shows identified item + image + client (who
ordered the sample) + client/vendor dates; Print SKU / Print address buttons → /api/print-sticker.
Auto-fires from applyScan on a resolved scan (best id: barcode > matched sku > mfr code).
- Cost throttle: scanTick slows 600ms→1300ms when the engine is cloud-only (no macvision), and
idle-pauses after 60s with tap-to-resume — caps Gemini spend on a forgotten-open scanner.
Files touched
Diff
commit 7f00ab46cab93e41dc090930d831c2fb75865ab8
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Mon Jul 6 17:16:32 2026 -0700
feat: sample-request sticker popup + scan cost throttle
- #sampleModal popup (reuses .hidden modal pattern): shows identified item + image + client (who
ordered the sample) + client/vendor dates; Print SKU / Print address buttons → /api/print-sticker.
Auto-fires from applyScan on a resolved scan (best id: barcode > matched sku > mfr code).
- Cost throttle: scanTick slows 600ms→1300ms when the engine is cloud-only (no macvision), and
idle-pauses after 60s with tap-to-resume — caps Gemini spend on a forgotten-open scanner.
---
public/index.html | 94 +++++++++++++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 91 insertions(+), 3 deletions(-)
diff --git a/public/index.html b/public/index.html
index 794fcac..5ab8cf8 100644
--- a/public/index.html
+++ b/public/index.html
@@ -128,6 +128,24 @@
.editor[hidden]{display:none}
.ed-stage{flex:1;min-height:0;display:flex;align-items:center;justify-content:center;padding:12px 12px 0;overflow:hidden}
#edcanvas{max-width:100%;max-height:100%;border-radius:12px;box-shadow:0 8px 40px rgba(0,0,0,.6);background:#000}
+ /* sample-request sticker popup */
+ .samp{position:fixed;inset:0;z-index:130;background:rgba(0,0,0,.6);display:flex;align-items:center;justify-content:center;padding:18px}
+ .samp[hidden]{display:none}
+ .samp-card{position:relative;width:100%;max-width:420px;background:#16140f;border:1px solid var(--line);border-radius:18px;padding:20px;box-shadow:0 20px 60px rgba(0,0,0,.6)}
+ .samp-x{position:absolute;top:12px;right:12px;background:none;border:none;color:var(--ink);font-size:20px;cursor:pointer;opacity:.7}
+ .samp-head{display:flex;gap:14px;align-items:center;margin-bottom:14px}
+ #sampImg{width:72px;height:72px;object-fit:cover;border-radius:12px;flex-shrink:0;background:#000}
+ .samp-pat{font-size:18px;font-weight:700;line-height:1.2}
+ .samp-meta{font-size:13px;color:#b7ad98;margin-top:3px}
+ .samp-client{border-top:1px solid var(--line);border-bottom:1px solid var(--line);padding:10px 0;margin-bottom:14px}
+ .samp-row{display:flex;justify-content:space-between;gap:12px;padding:4px 0;font-size:14px}
+ .samp-row span{color:#9a917d}
+ .samp-row b{font-weight:600;text-align:right}
+ .samp-btns{display:flex;flex-direction:column;gap:10px}
+ .samp-print{padding:14px;border-radius:12px;border:none;font-size:16px;font-weight:700;cursor:pointer;background:#2b7a3f;color:#fff}
+ .samp-print.samp-addr{background:#2d5b86}
+ .samp-print:disabled{opacity:.5;cursor:default}
+ .samp-note{font-size:12px;color:#9a917d;margin-top:10px;text-align:center;min-height:14px}
.ed-panel{flex:0 0 auto;background:var(--card);border-top:1px solid var(--line);padding:12px 14px max(14px,env(safe-area-inset-bottom));max-height:54vh;overflow-y:auto}
.ed-head{display:flex;align-items:center;gap:10px;margin-bottom:10px}
.ed-head .t{font:600 13px/1 "SF Pro Display",sans-serif;letter-spacing:.05em;text-transform:uppercase;color:var(--gold)}
@@ -283,6 +301,30 @@
</div>
</div>
</div>
+
+<!-- sample-request sticker popup: scan → who ordered this sample → print SKU / address sticker -->
+<div class="samp" id="sampleModal" hidden>
+ <div class="samp-card">
+ <button class="samp-x" id="sampClose">✕</button>
+ <div class="samp-head">
+ <img id="sampImg" alt="" hidden>
+ <div class="samp-id">
+ <div class="samp-pat" id="sampPat">—</div>
+ <div class="samp-meta" id="sampMeta"></div>
+ </div>
+ </div>
+ <div class="samp-client">
+ <div class="samp-row"><span>Client ordered</span><b id="sampClient">—</b></div>
+ <div class="samp-row"><span>Client date</span><b id="sampOrdered">—</b></div>
+ <div class="samp-row"><span>Vendor ordered</span><b id="sampVendorDate">—</b></div>
+ </div>
+ <div class="samp-btns">
+ <button class="samp-print" id="sampPrintSku">🎫 Print SKU sticker</button>
+ <button class="samp-print samp-addr" id="sampPrintAddr">✉️ Print address sticker</button>
+ </div>
+ <div class="samp-note" id="sampNote"></div>
+ </div>
+</div>
<script>
const $=s=>document.querySelector(s), grid=$('#grid');
let ITEMS=[], filter='twil', collapsed=false; // default landing = All TWIL
@@ -978,7 +1020,8 @@ document.querySelectorAll('.chip').forEach(c=>c.addEventListener('click',()=>{
}));
// 🔢 Scan a printed mfr#/SKU with the camera → OCR (server Vision) → jump to that product.
/* ===== LIVE scanner: stream the camera, OCR each frame, LOCK IN when a SKU repeats ===== */
-let scanStream=null,scanTimer=null,scanBusy=false,_lastTop=null,_lockHits=0,_frontHits=0;
+let scanStream=null,scanTimer=null,scanBusy=false,_lastTop=null,_lockHits=0,_frontHits=0,_scanStart=0,_scanSlow=false;
+const SCAN_FAST=600, SCAN_CLOUD=1300, SCAN_IDLE_MS=60000; // cloud OCR costs $ per frame → slow it + idle-pause
// shared camera choice (back/front) — drives BOTH the live scanner and every "Add Photo" capture input
let camFacing=LS('camFacing')||'environment';
function applyCamCapture(){ // point all <input capture> pickers at the chosen lens
@@ -1066,11 +1109,13 @@ async function openLiveScan(){
unlockAudio(); // we're inside the user's tap → unlock audio for the lock-in cue
try{ await startScanStream(); }
catch(e){ toast('Camera blocked — using snap'); return $('#scanInput').click(); }
- _lastTop=null;_lockHits=0;_frontHits=0; resetScanFields(); $('#scanlive').classList.remove('locked'); $('#scanStatus').textContent='Point at the printed SKU…';
+ _lastTop=null;_lockHits=0;_frontHits=0;_scanStart=Date.now();_scanSlow=false; resetScanFields(); $('#scanlive').classList.remove('locked'); $('#scanStatus').textContent='Point at the printed SKU…';
$('#scanFlip').textContent=(camFacing==='user'?'🔄 Back camera':'🔄 Front camera');
$('#scanlive').hidden=false; document.body.style.overflow='hidden';
- scanTimer=setInterval(scanTick,600);
+ scanTimer=setInterval(scanTick,SCAN_FAST);
}
+// tap the paused overlay to resume scanning (idle-pause caps cloud OCR cost)
+function resumeScan(){ if(!$('#scanlive').hidden && !scanTimer){ _scanStart=Date.now(); _lastTop=null;_lockHits=0;_frontHits=0; $('#scanStatus').textContent='Point at the printed SKU…'; scanTimer=setInterval(scanTick,_scanSlow?SCAN_CLOUD:SCAN_FAST); } }
async function flipCam(){ // toggle back<->front, persist, re-acquire live + re-point capture inputs
camFacing=(camFacing==='user')?'environment':'user'; setLS('camFacing',camFacing); applyCamCapture(); syncCamBtn();
$('#scanFlip').textContent=(camFacing==='user'?'🔄 Back camera':'🔄 Front camera');
@@ -1118,6 +1163,9 @@ async function scanTick(){
cRoi.getContext('2d').drawImage(v, roiX,roiY,roiW,roiH, 0,0,cRoi.width,cRoi.height);
const d=await ocrSmart(cRoi.toDataURL('image/jpeg',0.72)); // honed ROI; front-cam mirror-retry built in
if(d&&d.fields) updateScanFields(d.fields); // light SKU#/Model#/Name/Color chips green as they're read
+ // cost throttle: slow ticks on a cloud (paid) engine, and idle-pause to cap spend on a forgotten scanner
+ if(d&&d.available&&!d.available.includes('macvision')&&!_scanSlow){ _scanSlow=true; if(scanTimer){clearInterval(scanTimer);scanTimer=setInterval(scanTick,SCAN_CLOUD);} }
+ if(_scanStart&&(Date.now()-_scanStart)>SCAN_IDLE_MS&&!_lockHits&&!_frontHits){ if(scanTimer){clearInterval(scanTimer);scanTimer=null;} $('#scanStatus').textContent='⏸ Paused to save cost — tap to resume'; scanBusy=false; return; }
if(d&&d.top){
_frontHits=0; // a real code appeared → not the pattern face; cancel any front streak
if(d.top===_lastTop) _lockHits++; else {_lastTop=d.top;_lockHits=1;}
@@ -1210,7 +1258,47 @@ async function applyScan(d, imgDataUrl){
$('#q').value=hit.q;
if(hit.via==='any'){ chipTo('any'); toast('✓ '+hit.q+' — '+hit.n+' on TWIL'); doLookup(); }
else { chipTo('shop'); toast('✓ '+hit.q+' — '+hit.n+' in Shopify'); doShopSearch(); }
+ // sample-request: who ordered this sample? (best id: barcode > matched sku > mfr code) → sticker popup
+ checkSampleRequest((d&&d.fields&&d.fields.barcode)||hit.q, (d&&d.fields&&d.fields.model)||null);
+}
+
+// ── Sample-request sticker popup: scan → who ordered this sample → print SKU/address sticker (FileMaker) ──
+let _sampReq=null;
+function openSampleModal(data){
+ _sampReq=data;
+ const id=data.identity||{}, item=data.item||{}, cl=data.client||{}, dt=data.dates||{};
+ const img=$('#sampImg');
+ if(id.image){ img.src=id.image; img.hidden=false; } else { img.hidden=true; img.removeAttribute('src'); }
+ $('#sampPat').textContent=item.mfr_pattern||id.pattern||id.mfr_sku||item.combo_sku||'Sample';
+ $('#sampMeta').textContent=[id.vendor||item.vid, item.combo_sku].filter(Boolean).join(' · ');
+ $('#sampClient').textContent=cl.name||'(no client on record)';
+ $('#sampOrdered').textContent=dt.client_ordered||'—';
+ $('#sampVendorDate').textContent=dt.vendor_ordered||'—';
+ $('#sampNote').textContent=''; $('#sampPrintSku').disabled=false; $('#sampPrintAddr').disabled=false;
+ $('#sampleModal').hidden=false; document.body.style.overflow='hidden';
+}
+function closeSampleModal(){ $('#sampleModal').hidden=true; document.body.style.overflow=''; _sampReq=null; }
+async function sampPrint(type){
+ if(!_sampReq||!_sampReq.recordId) return;
+ $('#sampPrintSku').disabled=true; $('#sampPrintAddr').disabled=true; $('#sampNote').textContent='Sending to FileMaker…';
+ try{
+ const r=await(await fetch('/api/print-sticker',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({recordId:_sampReq.recordId,sticker:type})})).json();
+ if(r.ok){ $('#sampNote').textContent=r.queued?('✓ '+type+' sticker sent to FileMaker to print'):('(preview) would flag '+type+' — FM_WRITE=1 to go live'); toast('🎫 '+type+' → FileMaker'); }
+ else { $('#sampNote').textContent='✗ '+(r.err||'failed'); $('#sampPrintSku').disabled=false; $('#sampPrintAddr').disabled=false; }
+ }catch(e){ $('#sampNote').textContent='✗ '+e.message; $('#sampPrintSku').disabled=false; $('#sampPrintAddr').disabled=false; }
+}
+async function checkSampleRequest(code,mfr){
+ if(!code&&!mfr)return;
+ try{ const q=new URLSearchParams(); if(code)q.set('code',code); if(mfr)q.set('mfr',mfr);
+ const r=await(await fetch('/api/sample-request?'+q.toString())).json();
+ if(r&&r.found) openSampleModal(r);
+ }catch(e){}
}
+$('#sampClose').addEventListener('click',closeSampleModal);
+$('#sampPrintSku').addEventListener('click',()=>sampPrint('sku'));
+$('#sampPrintAddr').addEventListener('click',()=>sampPrint('address'));
+$('#scanvideo').addEventListener('click',resumeScan); // tap the paused overlay to resume scanning
+$('#scanStatus').addEventListener('click',resumeScan);
$('#scanCancel').addEventListener('click',closeLiveScan);
$('#scanFlip').addEventListener('click',flipCam);
// 🏷 Identify the brand by its LOGO + typesetting via the local VLM (qwen2.5vl). Slower
← 31dbd12 feat: FileMaker sample-request lookup + print-sticker handof
·
back to Dw Photo Capture
·
docs: FileMaker sticker setup — flag field + OnTimer poller 1f9cc3b →