[object Object]

← back to Dw Photo Capture

chore: lint, refactor, v1.6.1 (session close) — 5x batch harness: track() closure, dead cal/increment removed, eslint browser-globals for harness

26941e3fba3bd4664780222126e0f37dfb51dc9c · 2026-09-19 10:14:12 -0700 · Steve Abrams

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014D6DNRc992gKfEHCbt6urC

Files touched

Diff

commit 26941e3fba3bd4664780222126e0f37dfb51dc9c
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sat Sep 19 10:14:12 2026 -0700

    chore: lint, refactor, v1.6.1 (session close) — 5x batch harness: track() closure, dead cal/increment removed, eslint browser-globals for harness
    
    Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_014D6DNRc992gKfEHCbt6urC
---
 5x/features.cjs   | 26 ++++++++++----------------
 eslint.config.mjs |  6 ++++--
 package-lock.json |  4 ++--
 package.json      |  2 +-
 4 files changed, 17 insertions(+), 21 deletions(-)

diff --git a/5x/features.cjs b/5x/features.cjs
index 4451c54..c58aca8 100644
--- a/5x/features.cjs
+++ b/5x/features.cjs
@@ -441,6 +441,7 @@ async function tBatchApi() {
   const V = ['original', 'master', 'web'];
   const P = (side, k) => `/photos/batch/${sess}/${BATCH_SKU}_${side ? side + '_' : ''}${k}.jpg`;
   let accepted = 0;
+  const track = r => { if (r.d && r.d.ok) accepted++; return r; };   // counts accepted bPost calls (read once, by the B5 manifest-line check)
   try {
     // B0 — page + auth gate
     const a = await fetch(BATCH_ORIGIN + '/batch', { headers: bAuth }), n = await fetch(BATCH_ORIGIN + '/batch');
@@ -449,38 +450,33 @@ async function tBatchApi() {
       a.status === 200 && /text\/html/.test(a.headers.get('content-type') || '') && n.status === 401 && /id="tools"/.test(html) && /id="pv"/.test(html) && /id="sideBadge"/.test(html),
       `auth=${a.status} noauth=${n.status} tools=${/id="tools"/.test(html)} pv=${/id="pv"/.test(html)}`);
     // B1 — psku side → <SKU>_psku_<variant>.jpg, read-back 200
-    const b1 = await bPost({ sessionId: sess, sku: BATCH_SKU, seq: 1, vendor: '5x', collection: 'harness', original: front, master: front, web: front, meta: { side: 'psku', via: '5x' } });
-    if (b1.d && b1.d.ok) accepted++;
+    const b1 = track(await bPost({ sessionId: sess, sku: BATCH_SKU, seq: 1, vendor: '5x', collection: 'harness', original: front, master: front, web: front, meta: { side: 'psku', via: '5x' } }));
     const rb1 = await Promise.all(V.map(k => bHead(P('psku', k))));
     rec('B1', 'meta.side=psku → <SKU>_psku_{original,master,web}.jpg written + served', 'http', 'local',
       b1.status === 200 && b1.d && b1.d.ok === true && b1.d.sku === BATCH_SKU && V.every(k => b1.d.paths[k] === P('psku', k)) && rb1.every(s => s === 200) && (b1.d.errors || []).length === 0,
       `status=${b1.status} paths=${j(b1.d && b1.d.paths)} readback=${j(rb1)} errors=${j(b1.d && b1.d.errors)}`);
     const pskuBytesBefore = await bBytes(P('psku', 'original'));
     // B2 — info side, SAME sku, different image → both sets present, psku untouched
-    const b2 = await bPost({ sessionId: sess, sku: BATCH_SKU, seq: 1, vendor: '5x', collection: 'harness', original: back, master: back, web: back, meta: { side: 'info', via: '5x' } });
-    if (b2.d && b2.d.ok) accepted++;
+    const b2 = track(await bPost({ sessionId: sess, sku: BATCH_SKU, seq: 1, vendor: '5x', collection: 'harness', original: back, master: back, web: back, meta: { side: 'info', via: '5x' } }));
     const rb2 = await Promise.all(V.map(k => bHead(P('info', k))));
     const pskuBytesAfter = await bBytes(P('psku', 'original')), infoBytes = await bBytes(P('info', 'original'));
     rec('B2', 'meta.side=info on the SAME sku → <SKU>_info_*.jpg written; psku files NOT overwritten (both sets, distinct bytes)', 'http', 'local',
       b2.status === 200 && b2.d && b2.d.ok && V.every(k => b2.d.paths[k] === P('info', k)) && rb2.every(s => s === 200) && pskuBytesBefore > 0 && pskuBytesAfter === pskuBytesBefore && infoBytes > 0 && infoBytes !== pskuBytesBefore,
       `info=${j(rb2)} psku_orig ${pskuBytesBefore}→${pskuBytesAfter}B info_orig ${infoBytes}B`);
     // B3 — unsided → legacy naming
-    const b3 = await bPost({ sessionId: sess, sku: BATCH_SKU, seq: 2, original: front, master: front, web: front, meta: { via: '5x', single: true, side: null } });
-    if (b3.d && b3.d.ok) accepted++;
+    const b3 = track(await bPost({ sessionId: sess, sku: BATCH_SKU, seq: 2, original: front, master: front, web: front, meta: { via: '5x', single: true, side: null } }));
     const rb3 = await Promise.all(V.map(k => bHead(P('', k))));
     rec('B3', 'no side (single shot) → legacy <SKU>_{original,master,web}.jpg (back-compat)', 'http', 'local',
       b3.status === 200 && b3.d && b3.d.ok && V.every(k => b3.d.paths[k] === P('', k)) && rb3.every(s => s === 200), `paths=${j(b3.d && b3.d.paths)} readback=${j(rb3)}`);
     // B4 — unknown / hostile side → legacy naming, never in the filename
-    const b4a = await bPost({ sessionId: sess, sku: BATCH_SKU + '-B4', seq: 3, web: front, meta: { side: 'zzz' } });
-    const b4b = await bPost({ sessionId: sess, sku: BATCH_SKU + '-B4', seq: 3, web: front, meta: { side: '../../evil' } });
-    const b4c = await bPost({ sessionId: sess, sku: BATCH_SKU + '-B4', seq: 3, web: front, meta: { side: 42 } });
-    if (b4a.d && b4a.d.ok) accepted++; if (b4b.d && b4b.d.ok) accepted++; if (b4c.d && b4c.d.ok) accepted++;
+    const b4a = track(await bPost({ sessionId: sess, sku: BATCH_SKU + '-B4', seq: 3, web: front, meta: { side: 'zzz' } }));
+    const b4b = track(await bPost({ sessionId: sess, sku: BATCH_SKU + '-B4', seq: 3, web: front, meta: { side: '../../evil' } }));
+    const b4c = track(await bPost({ sessionId: sess, sku: BATCH_SKU + '-B4', seq: 3, web: front, meta: { side: 42 } }));
     const legacy = `/photos/batch/${sess}/${BATCH_SKU}-B4_web.jpg`;
     const okB4 = [b4a, b4b, b4c].every(r => r.status === 200 && r.d && r.d.ok && r.d.paths.web === legacy) && (await bHead(legacy)) === 200 && ![b4a, b4b, b4c].some(r => /zzz|evil|\.\.|42_/.test(j(r.d.paths)));
     rec('B4', 'unknown side (zzz / ../../evil / 42) → falls back to legacy naming; side never reaches the filename', 'http', 'local', okB4, `${j(b4a.d && b4a.d.paths)} | ${j(b4b.d && b4b.d.paths)} | ${j(b4c.d && b4c.d.paths)}`);
     // B5 — idempotent re-POST of the same side: same paths, still 200, manifest appends exactly one line per accepted POST
-    const b5 = await bPost({ sessionId: sess, sku: BATCH_SKU, seq: 1, original: front, master: front, web: front, meta: { side: 'psku' } });
-    if (b5.d && b5.d.ok) accepted++;
+    const b5 = track(await bPost({ sessionId: sess, sku: BATCH_SKU, seq: 1, original: front, master: front, web: front, meta: { side: 'psku' } }));
     const manifest = path.join(REPO, 'data', 'batch-sessions', sess + '.jsonl');
     let lines = null, parsed = true, sides = [];
     if (fs.existsSync(manifest)) { lines = fs.readFileSync(manifest, 'utf8').split('\n').filter(Boolean); for (const l of lines) { try { const o = JSON.parse(l); sides.push(o.meta && o.meta.side); if (o.sessionId !== sess) parsed = false; } catch (e) { parsed = false; } } }
@@ -490,8 +486,7 @@ async function tBatchApi() {
     if (lines === null) rec('B5m', 'manifest not found under repo data/batch-sessions (DATA dir differs?) — informational', 'http', 'local', 'skip', manifest);
     // B6 — degenerate blobs + traversal
     const tiny = 'data:image/jpeg;base64,' + Buffer.from('tiny').toString('base64');
-    const b6a = await bPost({ sessionId: sess, sku: BATCH_SKU + '-B6', original: tiny, master: front, meta: { side: 'psku' } });
-    if (b6a.d && b6a.d.ok) accepted++;
+    const b6a = await bPost({ sessionId: sess, sku: BATCH_SKU + '-B6', original: tiny, master: front, meta: { side: 'psku' } });   // not tracked: `accepted` is only consumed by the B5 check above
     const b6b = await bPost({ sessionId: sess, sku: BATCH_SKU + '-B6b', original: tiny });
     const b6c = await bPost({ sessionId: '../../../etc', sku: '../../server.js', web: front });
     const b6d = await bPost({ sessionId: '', sku: BATCH_SKU, web: front });
@@ -514,7 +509,6 @@ async function tBatchUi(browser, engine, vp) {
   if (batchIsProd()) { rec('B7', 'batch UI test REFUSED — origin looks like prod', engine, vp, false, BATCH_ORIGIN); return; }
   const sid = '5x-ui-' + Date.now();
   const sess = { id: sid, vendor: '5x Harness', collection: 'ui', date: '2026-09-19', total: 3, n: 0 };
-  const cal = { wbR: 1, wbG: 1, wbB: 1, lumaTarget: 120, grayLuma: 120, refLuma: 120, refCR: 1, refCB: 1, crop: { x: .1, y: .1, w: .8, h: .8 }, quad: [], settings: {}, at: new Date().toISOString() };
   // seed ONCE per tab (sessionStorage guard) so a reload keeps whatever the app persisted (tune / toolpanel)
   const init = `(function(){ try { if (sessionStorage.getItem('fivex_seeded')) return; sessionStorage.setItem('fivex_seeded','1'); } catch (e) { return; }
     localStorage.setItem('dwbatch.session', ${JSON.stringify(JSON.stringify(sess))});
@@ -585,7 +579,7 @@ async function tBatchUi(browser, engine, vp) {
     const t3 = await page.evaluate(() => ({ ls: JSON.parse(localStorage.getItem('dwbatch.tune') || 'null'), v: ['sBright', 'sWarm', 'sHue'].map(id => document.querySelector('#' + id).value) }));
     rec('B7.8', '↺ Reset colour → all sliders 0 and persisted 0', engine, vp, t3.ls && t3.ls.bright === 0 && t3.ls.warm === 0 && t3.ls.hue === 0 && t3.v.every(v => v === '0'), j(t3));
     // Resume path: the calibration IS persisted (dwbatch.cal.<id>), but does #bResume load it? (bStart does.)
-    await sleep(Math.max(CFG_TICK_GUESS, 400));
+    await sleep(CFG_TICK_GUESS);
     const calTxt = await page.evaluate(() => ({ chip: (document.querySelector('#calTxt') || {}).textContent, cls: (document.querySelector('#dCal') || {}).className }));
     const calStored = await page.evaluate(sid => !!localStorage.getItem('dwbatch.cal.' + sid), sid);
     rec('B7.10', 'Resume ("Pick up where I left off") reloads the persisted gray-card calibration → straight to SHOOT, CAL chip = ok (not none), no DRIFT overlay', engine, vp,
diff --git a/eslint.config.mjs b/eslint.config.mjs
index f5f1295..83ed860 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -45,8 +45,10 @@ export default [
   // page.$eval() execute in the BROWSER — so it legitimately references browser
   // globals (getComputedStyle, document, …). Give it both worlds.
   {
-    files: ["selfcheck-ui.cjs"],
-    languageOptions: { globals: { ...globals.node, ...globals.browser } },
+    files: ["selfcheck-ui.cjs", "5x/features.cjs"],   // 5x harness: same Playwright page.evaluate() pattern
+    languageOptions: { globals: { ...globals.node, ...globals.browser,
+      // index.html app-window state the 5x harness reads inside page.evaluate() (read-only there)
+      _frontPhoto: "readonly", _backPhoto: "readonly", _idSource: "readonly", _autoBusy: "readonly" } },
   },
 
   // Front-end: the <script> block extracted from index.html, browser globals.
diff --git a/package-lock.json b/package-lock.json
index 866e6d8..7a59813 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "dw-photo-capture",
-  "version": "1.6.0",
+  "version": "1.6.1",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "dw-photo-capture",
-      "version": "1.6.0",
+      "version": "1.6.1",
       "dependencies": {
         "amazon-cognito-identity-js": "^6.3.20",
         "pg": "^8.22.0"
diff --git a/package.json b/package.json
index 024732d..ae1cba8 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "dw-photo-capture",
-  "version": "1.6.0",
+  "version": "1.6.1",
   "private": true,
   "description": "Mobile DW SKU photo-capture tool. Zero RUNTIME deps — node server.js requires nothing. The deps below are DEV-ONLY (linting) and live only in gitignored node_modules.",
   "scripts": {

← 97b0233 5x: batch UI calibrates via real #cSave (no synthetic cal se  ·  back to Dw Photo Capture  ·  auto-data-snapshot: 2026-09-19T10:31:17 (3 data files) — 5x/ ed3e909 →