[object Object]

← back to Estimate Instant

estimate-instant: apply 5 contrarian fixes — always-gate leads/admin PII (ADMIN_CRED), targeted embed postMessage (host origin), unit-toggle value conversion, conservative per-strip half-drop math, total-linear-width hint

fb752d250aaa35d050b131ce1291f80b307c1f83 · 2026-07-16 16:37:35 -0700 · Steve

Files touched

Diff

commit fb752d250aaa35d050b131ce1291f80b307c1f83
Author: Steve <steve@designerwallcoverings.com>
Date:   Thu Jul 16 16:37:35 2026 -0700

    estimate-instant: apply 5 contrarian fixes — always-gate leads/admin PII (ADMIN_CRED), targeted embed postMessage (host origin), unit-toggle value conversion, conservative per-strip half-drop math, total-linear-width hint
---
 public/embed.js   |  3 ++-
 public/index.html | 13 +++++++++++--
 server.js         | 28 ++++++++++++++--------------
 3 files changed, 27 insertions(+), 17 deletions(-)

diff --git a/public/embed.js b/public/embed.js
index fe81dc2..da9c603 100644
--- a/public/embed.js
+++ b/public/embed.js
@@ -17,7 +17,8 @@
   if (!mount) { console.warn('[estimate-instant] embed target not found:', sel); return; }
 
   var f = document.createElement('iframe');
-  f.src = origin + '/?embed=1';
+  // Pass the host page origin so the widget targets its postMessage back to us (not '*').
+  f.src = origin + '/?embed=1&host=' + encodeURIComponent(location.origin);
   f.title = 'Wallpaper roll estimate';
   f.loading = 'lazy';
   f.setAttribute('scrolling', 'no');
diff --git a/public/index.html b/public/index.html
index 9dbbb45..0581ad1 100644
--- a/public/index.html
+++ b/public/index.html
@@ -53,6 +53,8 @@
   .test-banner b{font-weight:700}
   .standin-note{margin-top:6px;font-size:11px;color:#999;font-style:italic}
 
+  .hint{margin:-6px 0 16px;font-size:12px;color:#8a8578;line-height:1.45}
+  .hint b{color:#6a6558;font-weight:600}
   .foot{margin-top:20px;font-size:12px;color:#aaa;text-align:center}
   .foot a{color:#888}
 </style></head><body>
@@ -70,6 +72,7 @@
       <div class="fld"><label>Wall width <span id="uw">(ft)</span></label><input id="w" type="number" min="0" step="0.1" placeholder="e.g. 12"></div>
       <div class="fld"><label>Wall height <span id="uh">(ft)</span></label><input id="h" type="number" min="0" step="0.1" placeholder="e.g. 9"></div>
     </div>
+    <p class="hint">Wall width = the <b>total width of every wall</b> you're covering — add them up. Height is floor-to-ceiling.</p>
     <div class="fld"><label>Pattern</label><select id="pat"></select></div>
     <button class="go" id="go">Calculate rolls &amp; price</button>
 
@@ -124,8 +127,12 @@ async function loadRolls(){
   $('pat').innerHTML=rolls.map(x=>`<option value="${x.sku}">${x.pattern} — ${x.roll_width_in}" wide${x.pattern_repeat_in?`, ${x.pattern_repeat_in}" repeat`:', random match'}</option>`).join('');
 }
 $('units').addEventListener('click',e=>{ const b=e.target.closest('button'); if(!b)return;
+  const nu=b.dataset.u; if(nu===unit) return;
+  // Convert any entered values in place so toggling units never silently re-scales the number.
+  const conv = nu==='in' ? 12 : 1/12;
+  ['w','h'].forEach(id=>{ const el=$(id); const v=parseFloat(el.value); if(!isNaN(v)&&v>0) el.value=+(v*conv).toFixed(2); });
   [...$('units').children].forEach(x=>x.classList.remove('on')); b.classList.add('on');
-  unit=b.dataset.u; $('uw').textContent=$('uh').textContent=`(${unit})`; });
+  unit=nu; $('uw').textContent=$('uh').textContent=`(${unit})`; });
 
 function renderCheckout(r) {
   // r = estimate response with shopify fields
@@ -162,7 +169,9 @@ async function calc(){
   postHeight();
 }
 // When embedded via embed.js, report our content height so the host iframe can grow.
-function postHeight(){ if(parent!==window){ try{ parent.postMessage({dwEstimateHeight:document.body.scrollHeight+24},'*'); }catch(e){} } }
+// embed.js passes the host origin as ?host=… — target it explicitly, never broadcast to '*'.
+const EMBED_HOST=new URLSearchParams(location.search).get('host')||'';
+function postHeight(){ if(parent!==window && EMBED_HOST){ try{ parent.postMessage({dwEstimateHeight:document.body.scrollHeight+24},EMBED_HOST); }catch(e){} } }
 window.addEventListener('load',postHeight);
 $('go').addEventListener('click',calc);
 $('send').addEventListener('click',async()=>{
diff --git a/server.js b/server.js
index 71ed874..d9a8aee 100644
--- a/server.js
+++ b/server.js
@@ -30,7 +30,10 @@
 
 const http = require('http'), fs = require('fs'), path = require('path');
 const PORT = parseInt(process.env.PORT || '3900', 10);
-const BASIC_AUTH = process.env.BASIC_AUTH || ''; // "user:pass" to gate admin; empty = open
+const BASIC_AUTH = process.env.BASIC_AUTH || ''; // optional override for the admin credential
+// Admin surfaces (/admin + /api/leads = captured lead PII) are ALWAYS gated — even when the
+// public calculator runs open on an embed host. Default admin/DW2024!; override via env.
+const ADMIN_CRED = process.env.BASIC_AUTH || process.env.ADMIN_AUTH || 'admin:DW2024!';
 const DIR = __dirname;
 const ROLLS = path.join(DIR, 'data', 'rolls.json');
 const LEADS = path.join(DIR, 'data', 'leads.json');
@@ -54,10 +57,9 @@ function loadCatalog() {
 }
 
 function authed(req) {
-  if (!BASIC_AUTH) return true;
   const m = (req.headers.authorization || '').match(/^Basic\s+(.+)$/i);
   if (!m) return false;
-  try { return Buffer.from(m[1], 'base64').toString() === BASIC_AUTH; } catch { return false; }
+  try { return Buffer.from(m[1], 'base64').toString() === ADMIN_CRED; } catch { return false; }
 }
 
 // The core: rolls-needed with pattern-repeat + waste math (trade-standard strip method).
@@ -84,17 +86,15 @@ function estimate({ wallWidthIn, wallHeightIn, roll, checkout_domain }) {
   const rollLenIn = Number(roll.roll_length_ft) * 12;
   const repeat = Number(roll.pattern_repeat_in) || 0;
 
-  // Each strip must clear wall height + trim; for a patterned match, round the cut
-  // length UP to the next full pattern repeat so strips align side-to-side.
-  const rawCut = H + TRIM_ALLOWANCE_IN;
-  const cutLen = repeat > 0 ? Math.ceil(rawCut / repeat) * repeat : rawCut;
-
-  // Half-drop match: alternating strips start a half-repeat lower, so a half-repeat
-  // of each roll is consumed establishing the offset — reduces usable strips/roll
-  // (never under-counts, which would leave an installer short mid-wall).
+  // Each strip must clear wall height + trim, rounded UP to a full pattern repeat so
+  // strips align side-to-side. Half-drop match: adjacent strips are offset by half a
+  // repeat, so each strip needs an extra half-repeat of length to land at either offset
+  // — added BEFORE the round-up. Conservative by design: it rounds up and never
+  // under-counts (an under-count leaves the installer short mid-wall).
   const isHalfDrop = String(roll.match || '').toLowerCase() === 'half-drop' && repeat > 0;
-  const usableRollIn = isHalfDrop ? rollLenIn - repeat / 2 : rollLenIn;
-  const stripsPerRoll = Math.floor(usableRollIn / cutLen);
+  const rawCut = H + TRIM_ALLOWANCE_IN + (isHalfDrop ? repeat / 2 : 0);
+  const cutLen = repeat > 0 ? Math.ceil(rawCut / repeat) * repeat : rawCut;
+  const stripsPerRoll = Math.floor(rollLenIn / cutLen);
 
   if (stripsPerRoll < 1) {
     return {
@@ -244,6 +244,6 @@ http.createServer(async (req, res) => {
 }).listen(PORT, '127.0.0.1', function () {
   console.log('[estimate-instant] http://localhost:' + this.address().port);
   console.log('  Calculator: http://localhost:' + this.address().port + '/');
-  console.log('  Admin:      http://localhost:' + this.address().port + '/admin' + (BASIC_AUTH ? '  (auth required)' : '  (open — set BASIC_AUTH=user:pass to gate)'));
+  console.log('  Admin:      http://localhost:' + this.address().port + '/admin  (auth required — ADMIN_CRED)');
   console.log('  Embed demo: http://localhost:' + this.address().port + '/embed-demo.html');
 });

← f4c6d59 shopify: refresh catalog — keep sample-only as 'by request'  ·  back to Estimate Instant  ·  (newest)