← 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
M public/embed.jsM public/index.htmlM server.js
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 & 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)