← back to Dw Photo Capture
5x: batch UI calibrates via real #cSave (no synthetic cal seed); B7.10 resume-cal + B7.11 fall-through — 33/33 vs 5f0e9fc
97b02336f48e968e44ae5a87f6693f5adc63f21d · 2026-09-19 10:08:22 -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 97b02336f48e968e44ae5a87f6693f5adc63f21d
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Sat Sep 19 10:08:22 2026 -0700
5x: batch UI calibrates via real #cSave (no synthetic cal seed); B7.10 resume-cal + B7.11 fall-through — 33/33 vs 5f0e9fc
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014D6DNRc992gKfEHCbt6urC
---
5x/features.cjs | 33 ++++++++++++++++++++++++++++-----
1 file changed, 28 insertions(+), 5 deletions(-)
diff --git a/5x/features.cjs b/5x/features.cjs
index 39923c4..4451c54 100644
--- a/5x/features.cjs
+++ b/5x/features.cjs
@@ -518,14 +518,34 @@ async function tBatchUi(browser, engine, vp) {
// 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))});
- localStorage.setItem('dwbatch.cal.${sid}', ${JSON.stringify(JSON.stringify(cal))});
- localStorage.removeItem('dwbatch.tune'); localStorage.removeItem('dwbatch.toolpanel'); })();`;
+ /* NO synthetic dwbatch.cal seed: a fake cal never matches the fake camera's reference band, so the
+ app (correctly) fires DRIFT + the RE-CALIBRATE overlay and blocks every click. The harness instead
+ calibrates through the REAL UI (#cSave) on the first resume, so the stored cal is self-consistent. */
+ localStorage.removeItem('dwbatch.cal.${sid}'); localStorage.removeItem('dwbatch.tune'); localStorage.removeItem('dwbatch.toolpanel'); })();`;
const c = await newCtx(browser, engine, vp, { init }); const { page, ctx } = c;
const writes = [], ocr = [];
await ctx.route('**/api/batch-shot', r => { writes.push(1); return r.fulfill({ status: 200, contentType: 'application/json', body: j({ ok: true, sku: 'FAKE', paths: {}, errors: [] }) }); });
await ctx.route('**/api/identify**', r => { ocr.push(1); return r.fulfill({ status: 200, contentType: 'application/json', body: j({ ok: true, sku: null, confidence: 0, cost_usd: 0 }) }); });
- const resume = async () => { await page.goto(BATCH_ORIGIN + '/batch', { waitUntil: 'domcontentloaded' }); await page.waitForSelector('#bResume:not([hidden])', { timeout: 15000 }); await page.click('#bResume'); await page.waitForSelector('#vShoot:not([hidden])', { timeout: 10000 });
- if (await page.$eval('#gate', e => !e.hidden)) { await page.click('#startCam'); await page.waitForFunction(() => document.querySelector('#gate').hidden, null, { timeout: 15000 }).catch(() => {}); } };
+ // Resume → either the CALIB view (no stored cal → app falls through to enterCalib, per 5f0e9fc) or
+ // straight to SHOOT (stored cal loaded). Returns which path the app took so B7.10/B7.11 can assert it.
+ const resumePaths = [];
+ const resume = async () => {
+ await page.goto(BATCH_ORIGIN + '/batch', { waitUntil: 'domcontentloaded' });
+ await page.waitForSelector('#bResume:not([hidden])', { timeout: 15000 }); await page.click('#bResume');
+ await page.waitForFunction(() => !document.querySelector('#vShoot').hidden || !document.querySelector('#vCalib').hidden, null, { timeout: 10000 });
+ const inCalib = await page.$eval('#vCalib', e => !e.hidden);
+ resumePaths.push(inCalib ? 'calib' : 'shoot');
+ if (inCalib) { // real calibration with the fake camera: start cam → wait for frames → Capture calibration (#cSave)
+ if (await page.$eval('#cGate', e => !e.hidden)) { await page.click('#cStartCam'); }
+ await page.waitForFunction(() => { const v = document.querySelector('#cv'); return v && v.videoWidth > 0 && document.querySelector('#cGate').hidden; }, null, { timeout: 15000 });
+ await sleep(300); await page.click('#cSave');
+ await page.waitForSelector('#vShoot:not([hidden])', { timeout: 10000 });
+ }
+ if (await page.$eval('#gate', e => !e.hidden)) { await page.click('#startCam'); await page.waitForFunction(() => document.querySelector('#gate').hidden, null, { timeout: 15000 }).catch(() => {}); }
+ // if a stale/mismatched cal ever trips DRIFT here the RE-CAL overlay would block every click — surface it, never hang
+ await sleep(500);
+ if (await page.$eval('#bigRecal', e => !e.hidden)) throw new Error('RE-CALIBRATE overlay shown after resume: ' + await page.$eval('#recalMeta', e => e.textContent));
+ };
await guarded('B7', engine, vp, async () => {
await resume();
const s = await page.evaluate(() => { const q = s => document.querySelector(s); const vis = e => { if (!e) return false; const r = e.getBoundingClientRect(); return r.width > 0 && r.height > 0 && getComputedStyle(e).display !== 'none'; };
@@ -567,7 +587,10 @@ async function tBatchUi(browser, engine, vp) {
// Resume path: the calibration IS persisted (dwbatch.cal.<id>), but does #bResume load it? (bStart does.)
await sleep(Math.max(CFG_TICK_GUESS, 400));
const calTxt = await page.evaluate(() => ({ chip: (document.querySelector('#calTxt') || {}).textContent, cls: (document.querySelector('#dCal') || {}).className }));
- rec('B7.10', 'Resume ("Pick up where I left off") reloads the persisted gray-card calibration (CAL chip = ok, not none)', engine, vp, /^ok$/i.test((calTxt.chip || '').trim()), j(calTxt) + ' — a stored dwbatch.cal.<sessionId> exists for this session');
+ 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,
+ /^ok$/i.test((calTxt.chip || '').trim()) && calStored && resumePaths.length >= 2 && resumePaths.slice(1).every(pth => pth === 'shoot'), j({ ...calTxt, calStored, resumePaths }));
+ rec('B7.11', 'Resume with NO stored calibration falls through to the CALIB view (same as Start) and #cSave stores a self-consistent cal', engine, vp, resumePaths[0] === 'calib' && calStored, j({ first: resumePaths[0], calStored }));
rec('B7.9', 'harness guard: no real /api/batch-shot writes, no /api/identify (Gemini) calls reached the server from the UI test', engine, vp, true, `batch-shot intercepted=${writes.length} identify intercepted=${ocr.length}`);
});
await finishCtx('batch shoot UI', engine, vp, c);
← 5f0e9fc Batch shoot: Resume now restores calibration (was shooting w
·
back to Dw Photo Capture
·
chore: lint, refactor, v1.6.1 (session close) — 5x batch har 26941e3 →