← 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
M fm-client.jsM server.js
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 →