← back to Claude Webdev Accelerator
clients/estimate-instant-contrarian-handoff.md
54 lines
# estimate-instant — contrarian handoff (from the yielded accelerator session)
Two Claude sessions built `estimate-instant` concurrently on 2026-07-15/16. This
session **yielded** to the active one (Steve's call) after running the playbook's
verify + contrarian gate. Captured here so the fixes aren't lost. Checked against
the active session's HEAD `646c3eb` — **all 5 remain open**.
## Verified working (this session, hand-checked)
- Roll-math `estimate()`: 120"×96" Heirloom Damask → 2 rolls / $192 / 26% waste ✓;
grasscloth 200"×108" → 3 rolls / $384 / 25% ✓; too-tall + zero-input guards fire ✓.
- Embeddable widget `embed.js` (iframe + auto-height postMessage) — shipped, matches
the brief's 200+-site embed thesis.
## STATUS: all 5 fixes APPLIED + browser-verified — commit `fb752d2` (2026-07-16). Section below kept for the record.
## Contrarian verdict: FIX FIRST — 5 open items (highest leverage first)
1. **Leads PII exposed in public mode** — `server.js:57` `if (!BASIC_AUTH) return true`.
`/api/leads` + `/admin` are gated only when `BASIC_AUTH` is set. On any embed host
that runs the estimator open (the whole point of the widget), the leads store is
world-readable. Fix: a separate always-on `ADMIN_AUTH` (default `admin:DW2024!`)
that gates `/api/leads` + `/admin` independent of the public-preview `BASIC_AUTH`.
2. **postMessage targets `'*'`** — `public/index.html:165`. Send height to the known
host origin instead. Pass it in: embed.js `f.src = origin + '/?embed=1&host=' +
encodeURIComponent(location.origin)`; index reads `host` and targets it.
3. **Unit toggle silently mis-scales** — `public/index.html:128` only relabels ft/in;
the typed value is unchanged, so toggling ft→in feeds e.g. `12` back through the
inch path. Fix: convert the current `#w`/`#h` values in place on toggle (×12 / ÷12),
or clear them.
4. **Half-drop math is an approximation stated with false confidence** —
`server.js:96` docks `repeat/2` from usable roll length. The defensible
never-short model is per-strip: `cutLen += repeat/2` before rounding up to the
full repeat. Minimum honest fix if not reworking: say so in the disclaimer
("half-drop / large-repeat patterns — add ~10–15%").
5. **Single "wall width" invites under-ordering a room** — `public/index.html:70`.
Add a hint: "total width of all walls to cover (linear footage)". One sentence
prevents every non-trade user from ordering for their widest wall only.
## New surface to gate (added by the active session, needs its own review)
- Shopify **"Buy N rolls" checkout CTA** hands off to the **live** Shopify checkout
(TEST banner + stand-in-variant logic present). Live checkout = customer-facing /
money → verify the variant mapping and keep it Steve-gated before any public embed.
## Data note (already handled by the active session)
- Real roll specs now pull via `scripts/pull-rollspecs.sql` from `dw_unified`. Seed
`data/rolls.json` remains the labeled-representative fallback. `dw_unified`
wallcovering roll metafields are messy free-text (e.g. `length:"Sisal"`,
`width:"36 inches (trim to 34\")"`) — the SQL must normalize width/length/repeat,
not trust the raw metafield strings.