[object Object]

← back to Dw Photo Capture

fix: print-sticker no longer hangs + go-live hardening

7754221d4a607f8ee3588766743fd95847ec61f3 · 2026-07-06 17:38:55 -0700 · Steve Abrams

- ROOT CAUSE: fmUpdate's pre-read (fmGet) stalls in-request on records with big portals. fm() now
  has a 12s AbortController timeout (no FM call can hang a request); added fmSet (direct PATCH, no
  pre-read). print-sticker: dryRun returns the intended write with ZERO FM calls; live uses fmSet.
- todayMDY() now computes the Pacific date (Kamatera is UTC) so the stamp never records tomorrow.
- Deployed; writes stay dryRun (FM_WRITE unset) until Steve flips it on his business data.

Files touched

Diff

commit 7754221d4a607f8ee3588766743fd95847ec61f3
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon Jul 6 17:38:55 2026 -0700

    fix: print-sticker no longer hangs + go-live hardening
    
    - ROOT CAUSE: fmUpdate's pre-read (fmGet) stalls in-request on records with big portals. fm() now
      has a 12s AbortController timeout (no FM call can hang a request); added fmSet (direct PATCH, no
      pre-read). print-sticker: dryRun returns the intended write with ZERO FM calls; live uses fmSet.
    - todayMDY() now computes the Pacific date (Kamatera is UTC) so the stamp never records tomorrow.
    - Deployed; writes stay dryRun (FM_WRITE unset) until Steve flips it on his business data.
---
 fm-client.js | 24 +++++++++++++++++++-----
 server.js    | 23 +++++++++++++++++------
 2 files changed, 36 insertions(+), 11 deletions(-)

diff --git a/fm-client.js b/fm-client.js
index 66bbe92..db347d0 100644
--- a/fm-client.js
+++ b/fm-client.js
@@ -51,10 +51,17 @@ async function getSession(db) {
 
 async function fm(db, path, { method = 'GET', body } = {}) {
   const token = await getSession(db);
-  const res = await fetch(`${base(db)}${path}`, {
-    method, headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
-    body: body ? JSON.stringify(body) : undefined,
-  });
+  // Hard timeout so a slow/stuck Data API call can never hang an HTTP request forever.
+  const ctl = new AbortController();
+  const timer = setTimeout(() => ctl.abort(), Number(process.env.FM_TIMEOUT_MS) || 12000);
+  let res;
+  try {
+    res = await fetch(`${base(db)}${path}`, {
+      method, headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
+      body: body ? JSON.stringify(body) : undefined, signal: ctl.signal,
+    });
+  } catch (e) { throw new Error(e.name === 'AbortError' ? `FileMaker timeout (${db}${path})` : e.message); }
+  finally { clearTimeout(timer); }
   const json = await res.json().catch(() => ({}));
   const msg = json && json.messages && json.messages[0];
   if (msg && msg.code && msg.code !== '0') {
@@ -90,4 +97,11 @@ async function fmUpdate(db, layout, recordId, fieldData, { dryRun = true } = {})
   return { committed: true, recordId, changes: diff };
 }
 
-module.exports = { getIdToken, fmFind, fmGet, fmUpdate };
+// Direct PATCH with NO pre-read (fmGet on a record with big portals can be slow/stuck in a request
+// handler). Use when you just need to set fields and don't need a before/after diff.
+async function fmSet(db, layout, recordId, fieldData) {
+  await fm(db, `/layouts/${encodeURIComponent(layout)}/records/${encodeURIComponent(recordId)}`, { method: 'PATCH', body: { fieldData } });
+  return { committed: true, recordId, wrote: fieldData };
+}
+
+module.exports = { getIdToken, fmFind, fmGet, fmUpdate, fmSet };
diff --git a/server.js b/server.js
index 5e9875b..ec5476e 100644
--- a/server.js
+++ b/server.js
@@ -22,6 +22,9 @@ const FM_DB = process.env.FM_DB || 'WALLPAPER';
 const FM_LAYOUT = process.env.FM_LAYOUT || 'Sample Requests via email';
 const FM_PRINT_FLAG = process.env.FM_PRINT_FLAG_FIELD || '';   // Steve designates a flag field the Mac poller reads
 const FM_ENABLED = () => !!(FM && process.env.FM_CLARIS_EMAIL && process.env.FM_CLARIS_PASSWORD && process.env.FM_CLOUD_HOST);
+// Today's date as MM/DD/YYYY in the business timezone (Kamatera runs UTC; stamp must be Pacific
+// so an evening scan never records tomorrow's date on the FileMaker record). Override via FM_TZ.
+function todayMDY() { return new Date().toLocaleDateString('en-US', { timeZone: process.env.FM_TZ || 'America/Los_Angeles', year: 'numeric', month: '2-digit', day: '2-digit' }); }
 
 const ROOT = __dirname;
 const DATA = path.join(ROOT, 'data');
@@ -1080,9 +1083,9 @@ const appHandler = (req, res) => {
     return;
   }
 
-  // Print-sticker handoff: flag the FileMaker record so the Mac's FileMaker Pro poller runs the
-  // EXISTING print-sticker + stamp scripts (stamp Date WP Sample Sent = today + print the Zebra
-  // label). Write is dryRun unless FM_WRITE=1 (protects the live business file).
+  // Print-sticker "send": STAMP the sample as sent (Date WP Sample Sent = today — Steve's "field to
+  // fill in") and, if a flag field is configured, ALSO flag the record so the Mac's FileMaker Pro
+  // poller runs the physical Zebra print. Write is dryRun unless FM_WRITE=1 (protects the live file).
   if (u.pathname === '/api/print-sticker' && req.method === 'POST') {
     let body = ''; req.on('data', c => { body += c; if (body.length > 16 * 1024) req.destroy(); });
     req.on('end', () => {
@@ -1091,10 +1094,18 @@ const appHandler = (req, res) => {
       const sticker = p.sticker === 'address' ? 'address' : 'sku';
       if (!recordId) return send(res, 400, { err: 'recordId required' });
       if (!FM_ENABLED()) return send(res, 200, { ok: false, err: 'FileMaker not configured' });
-      if (!FM_PRINT_FLAG) return send(res, 200, { ok: false, err: 'FM_PRINT_FLAG_FIELD not set — designate a flag field the FileMaker poller reads' });
       const dryRun = process.env.FM_WRITE !== '1';
-      FM.fmUpdate(FM_DB, FM_LAYOUT, recordId, { [FM_PRINT_FLAG]: sticker }, { dryRun })
-        .then(r => send(res, 200, { ok: true, queued: !dryRun, dryRun, sticker, result: r }))
+      const fieldData = {};
+      // Stamp the sent date (US MM/DD/YYYY, matching the file's date format) unless disabled.
+      if (process.env.FM_STAMP_SENT !== '0') fieldData[process.env.FM_SENT_FIELD || 'Date WP Sample Sent'] = todayMDY();
+      // Optional poller flag for the physical Zebra print (Mac FileMaker Pro side).
+      if (FM_PRINT_FLAG) fieldData[FM_PRINT_FLAG] = sticker;
+      if (!Object.keys(fieldData).length) return send(res, 200, { ok: false, err: 'nothing to write (FM_STAMP_SENT=0 and no FM_PRINT_FLAG_FIELD)' });
+      // dryRun: report the intended write WITHOUT touching FileMaker (a pre-read on records with big
+      // portals can stall). Live: direct PATCH via fmSet (no pre-read) so it's fast and can't hang.
+      if (dryRun) return send(res, 200, { ok: true, committed: false, dryRun: true, sticker, wrote: fieldData });
+      FM.fmSet(FM_DB, FM_LAYOUT, recordId, fieldData)
+        .then(() => send(res, 200, { ok: true, committed: true, dryRun: false, sticker, wrote: fieldData }))
         .catch(e => send(res, 200, { ok: false, err: e.message }));
     });
     return;

← 1f9cc3b docs: FileMaker sticker setup — flag field + OnTimer poller  ·  back to Dw Photo Capture  ·  5x sweep: lazy-load grid thumbnails (IntersectionObserver) — 060b63d →