← 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
M 5x/features.cjsM eslint.config.mjsM package-lock.jsonM package.json
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 →