[object Object]

← back to Dw Photo Capture

Consolidate SKU field cleanup into shared field-clean.js helper

2b1d0068b8c929e96d2e142626a8164f658bf090 · 2026-09-24 13:45:38 -0700 · Steve Abrams

All four simple-*.html pages now use a common cleanField(v) helper to coerce
Gemini Vision JSON string fields (which arrive as numbers or nulls) to trimmed
strings before assignment. This replaces inline String(v).trim() or no coercion
at all, preventing TypeError crashes when OCR returns unexpected types.

- Created public/js/field-clean.js with a single exported function
- Wired all four pages (minimal, wizard, probooth, instant) to load it
- Updated fillIfEmpty in probooth.html to use cleanField
- Updated simple-instant.html line 361 to use cleanField for mfr_sku
- All four pages parse correctly with helper loaded before use

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014WCvaA6QQCJp2LMdcXyrUD

Files touched

Diff

commit 2b1d0068b8c929e96d2e142626a8164f658bf090
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Sep 24 13:45:38 2026 -0700

    Consolidate SKU field cleanup into shared field-clean.js helper
    
    All four simple-*.html pages now use a common cleanField(v) helper to coerce
    Gemini Vision JSON string fields (which arrive as numbers or nulls) to trimmed
    strings before assignment. This replaces inline String(v).trim() or no coercion
    at all, preventing TypeError crashes when OCR returns unexpected types.
    
    - Created public/js/field-clean.js with a single exported function
    - Wired all four pages (minimal, wizard, probooth, instant) to load it
    - Updated fillIfEmpty in probooth.html to use cleanField
    - Updated simple-instant.html line 361 to use cleanField for mfr_sku
    - All four pages parse correctly with helper loaded before use
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_014WCvaA6QQCJp2LMdcXyrUD
---
 public/js/field-clean.js    | 13 +++++++++++++
 public/simple-instant.html  |  3 ++-
 public/simple-minimal.html  |  5 +++--
 public/simple-probooth.html |  3 ++-
 public/simple-wizard.html   |  3 ++-
 5 files changed, 22 insertions(+), 5 deletions(-)

diff --git a/public/js/field-clean.js b/public/js/field-clean.js
new file mode 100644
index 0000000..5826112
--- /dev/null
+++ b/public/js/field-clean.js
@@ -0,0 +1,13 @@
+/*
+ * field-clean.js — shared string-field coercion for OCR/form workflows.
+ *
+ * Gemini Vision JSON parsing can return what should be string fields as numbers or nulls:
+ * a manufacturer SKU like "51526" arrives as the number 51526, a missing field as null instead of "".
+ * Before calling .trim() or .replace() on these fields, coerce to string and handle nulls.
+ *
+ * This helper is used across all simple-*.html capture flows to prevent "Cannot read property 'trim'
+ * of undefined" crashes when Gemini returns unexpected types.
+ */
+function cleanField(v) {
+  return String(v == null ? '' : v).trim();
+}
diff --git a/public/simple-instant.html b/public/simple-instant.html
index e13f286..8e5b946 100644
--- a/public/simple-instant.html
+++ b/public/simple-instant.html
@@ -243,6 +243,7 @@
   <div class="flash-overlay" id="flashOverlay"></div>
   <div class="toast" id="toast"></div>
 
+<script src="/js/field-clean.js"></script>
 <script>
 (function(){
   var $ = function(id){ return document.getElementById(id); };
@@ -358,7 +359,7 @@
       if (!fVendor.value.trim() && (data.vendor_matched || fields.vendor)){
         fVendor.value = data.vendor_matched || fields.vendor; vendorFromExtract = true;
       }
-      if (!fMfr.value.trim() && fields.mfr_sku) fMfr.value = fields.mfr_sku;
+      if (!fMfr.value.trim() && fields.mfr_sku != null) fMfr.value = cleanField(fields.mfr_sku);
       if (!fPattern.value.trim() && (fields.pattern_name || fields.color)){
         fPattern.value = [fields.pattern_name, fields.color].filter(Boolean).join(' — ');
       }
diff --git a/public/simple-minimal.html b/public/simple-minimal.html
index defb99c..dfbe554 100644
--- a/public/simple-minimal.html
+++ b/public/simple-minimal.html
@@ -34,6 +34,7 @@
 <link rel="apple-touch-icon" sizes="180x180" href="/icon-180.png">
 <link rel="icon" type="image/png" sizes="192x192" href="/icon-192.png">
 <title>DW Photo — Minimal Showroom</title>
+<script src="/js/field-clean.js"></script>
 <style>
   :root{ --bg:#0f0e0c; --panel:#1b1710; --card:#1b1916; --ink:#f3efe7; --muted:#9a9184;
          --line:#2e2a25; --gold:#c8a24a; --green:#3fa06a; --red:#c0563f; }
@@ -329,7 +330,7 @@
         var f = (r && r.fields) || {};
         lastExtractFields = f;
         var vendor = vendorOverrideSel.value || r.vendor_matched || '';
-        var mfr = String(f.mfr_sku == null ? '' : f.mfr_sku).trim();   // Gemini JSON may type a numeric SKU as a number
+        var mfr = cleanField(f.mfr_sku);
         if (vendor && mfr && AUTO_CREATE) {
           procMsg.textContent = 'Creating ' + vendor + ' item…';
           createItem(dataUrl, vendor, mfr, f);
@@ -372,7 +373,7 @@
     rcDetail.textContent = (readErr ? 'Label read failed (' + readErr + '). ' : '') + 'Pick the vendor and type the mfr# to save this item.';
     rcTime.hidden = !SHOW_TIMING; rcTime.textContent = 'read: ' + fmtMs(elapsed);
     rcManual.classList.add('on');
-    mMfr.value = fields && fields.mfr_sku != null ? String(fields.mfr_sku) : '';
+    mMfr.value = cleanField(fields.mfr_sku);
     rcSave.onclick = function(){
       var v = mVendor.value.trim(), m = mMfr.value.trim();
       mVendor.classList.toggle('need', !v); mMfr.classList.toggle('need', !m);
diff --git a/public/simple-probooth.html b/public/simple-probooth.html
index 592b72b..779a5cc 100644
--- a/public/simple-probooth.html
+++ b/public/simple-probooth.html
@@ -244,6 +244,7 @@
 
 <script src="/js/capture-pipeline.js"></script>
 <script src="/js/acquire-camera.js"></script>
+<script src="/js/field-clean.js"></script>
 <script>
 'use strict';
 /* Pro Booth client — see the header comment for the speed architecture. */
@@ -381,7 +382,7 @@ function fireExtract(url){
   });
 }
 let extracted={};
-function fillIfEmpty(id,v){ const el=$('#'+id); if(el && !el.value.trim() && v) el.value=String(v).trim(); }
+function fillIfEmpty(id,v){ const el=$('#'+id); const c=cleanField(v); if(el && !el.value.trim() && c) el.value=c; }
 function showTiming(){
   const t=[];
   if(perf.bakeMs!=null) t.push('bake '+perf.bakeMs+'ms @ '+perf.px);
diff --git a/public/simple-wizard.html b/public/simple-wizard.html
index 7398c25..7d4a91c 100644
--- a/public/simple-wizard.html
+++ b/public/simple-wizard.html
@@ -9,6 +9,7 @@
 <meta name="apple-mobile-web-app-title" content="DW Capture">
 <link rel="apple-touch-icon" href="/icon-180.png">
 <link rel="icon" href="/icon-192.png">
+<script src="/js/field-clean.js"></script>
 <style>
   /* ============================================================================================
      GUIDED WIZARD — TK-12162 concept-guided-wizard
@@ -455,7 +456,7 @@
     if(extractPromise){ extractResult = await extractPromise; }
     const fields = (extractResult && extractResult.fields) || {};
     const vendor = extractResult && (extractResult.vendor_matched || fields.vendor) || '';
-    const mfr = String(fields.mfr_sku == null ? '' : fields.mfr_sku).trim();   // Gemini JSON may type a numeric SKU as a number
+    const mfr = cleanField(fields.mfr_sku);
 
     if(!mfr || !vendor){
       // Honest graceful degradation — the label didn't read clearly enough to auto-identify.

← 10d3e40 TK-12162: guided wizard never strands a guest on an unexpect  ·  back to Dw Photo Capture  ·  TK-12162: capture at high res on all simple-* pages + route 14e3194 →