← back to Dw Photo Capture
photo-capture #8 harden (TK-11962 DTD): TTL-guard stale restore + surface quota failure
c5ee50a445ff45afbe044f2fefd89346a32ae3b6 · 2026-09-21 10:38:36 -0700 · Steve Abrams
Cody's dissent (verified) found two holes in the c87b661 sessionStorage capture
persistence that made shipping index.html risky:
- restoreCaptureState() auto-fired on every load with NO staleness check (ts stored
but never read) -> a stale in-tab capture could silently reopen and be attached to
the WRONG SKU (the exact hazard the scanner exists to prevent).
- full-res photo dataURLs overflow iOS Safari's small sessionStorage cap; the bare
catch{} swallowed QuotaExceededError silently while claiming photos were preserved.
Fix (still index.html only, additive):
- _CAP_TTL_MS=2h + _capFresh(): hasSavedCapture() and restoreCaptureState() now
discard (and clear) any capture older than 2h or lacking a ts -> stale never restores.
- saveCaptureState() catch surfaces a one-shot toast so a storage-full failure is
VISIBLE instead of a silent no-op; re-armed on the next successful save.
Verified: TTL unit test (fresh<2h restore / stale>2h + ts-less reject) all pass;
eslint 0 errors; page serves 200 with guards live. Local only; prod deploy stays gated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D1JYdvZfHHAcizjE4LY79X
Files touched
Diff
commit c5ee50a445ff45afbe044f2fefd89346a32ae3b6
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Mon Sep 21 10:38:36 2026 -0700
photo-capture #8 harden (TK-11962 DTD): TTL-guard stale restore + surface quota failure
Cody's dissent (verified) found two holes in the c87b661 sessionStorage capture
persistence that made shipping index.html risky:
- restoreCaptureState() auto-fired on every load with NO staleness check (ts stored
but never read) -> a stale in-tab capture could silently reopen and be attached to
the WRONG SKU (the exact hazard the scanner exists to prevent).
- full-res photo dataURLs overflow iOS Safari's small sessionStorage cap; the bare
catch{} swallowed QuotaExceededError silently while claiming photos were preserved.
Fix (still index.html only, additive):
- _CAP_TTL_MS=2h + _capFresh(): hasSavedCapture() and restoreCaptureState() now
discard (and clear) any capture older than 2h or lacking a ts -> stale never restores.
- saveCaptureState() catch surfaces a one-shot toast so a storage-full failure is
VISIBLE instead of a silent no-op; re-armed on the next successful save.
Verified: TTL unit test (fresh<2h restore / stale>2h + ts-less reject) all pass;
eslint 0 errors; page serves 200 with guards live. Local only; prod deploy stays gated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D1JYdvZfHHAcizjE4LY79X
---
public/index.html | 15 +++++++++++++--
1 file changed, 13 insertions(+), 2 deletions(-)
diff --git a/public/index.html b/public/index.html
index 9318423..0c17dc4 100644
--- a/public/index.html
+++ b/public/index.html
@@ -1946,16 +1946,27 @@ function addResetMedia(){ _media=[]; _addPhoto=null; _frontPhoto=null; _backPhot
// restores the in-progress item instead of losing an operator's two photos.
const _CAP_KEY='dwcap.inprogress';
const _CAP_FIELDS=['addMfr','addName','addColor','addPrice','addWidth','addRepeat','addContent','addHowSold','addNotes'];
+const _CAP_TTL_MS=2*60*60*1000; // TK-11962: never auto-restore a capture older than 2h (stale restore = wrong-photo/wrong-SKU risk)
+let _capSaveWarned=false; // one-shot so a quota failure surfaces once, not on every shot/keystroke
+function _capFresh(d){ return !!(d && d.ts && (Date.now()-d.ts) < _CAP_TTL_MS); }
function clearCaptureState(){ try{ sessionStorage.removeItem(_CAP_KEY); }catch(e){} }
-function hasSavedCapture(){ try{ const s=sessionStorage.getItem(_CAP_KEY); if(!s) return false; const d=JSON.parse(s); return !!(d && (d.front||d.back||(d.media&&d.media.length))); }catch(e){ return false; } }
+function hasSavedCapture(){ try{ const s=sessionStorage.getItem(_CAP_KEY); if(!s) return false; const d=JSON.parse(s);
+ if(!_capFresh(d)){ clearCaptureState(); return false; } // TK-11962: stale (>2h) or ts-less → discard, never auto-restore
+ return !!(d && (d.front||d.back||(d.media&&d.media.length))); }catch(e){ return false; } }
function saveCaptureState(){ try{
if(typeof _addMode!=='undefined' && _addMode==='update') return; // update-existing flow is not a new-item capture
if(!_frontPhoto && !_backPhoto && !(_media&&_media.length)){ clearCaptureState(); return; }
const fields={}; _CAP_FIELDS.forEach(id=>{ const el=$('#'+id); if(el) fields[id]=el.value; });
sessionStorage.setItem(_CAP_KEY, JSON.stringify({ v:1, ts:Date.now(), front:_frontPhoto, back:_backPhoto, media:_media, addPhoto:_addPhoto, idSource:_idSource, extracted:_addExtracted, fields }));
-}catch(e){} }
+ _capSaveWarned=false; // a save succeeded → re-arm the warning for any future failure
+}catch(e){
+ // TK-11962: iOS Safari's small per-origin sessionStorage cap can reject two full-res photos.
+ // Surface the otherwise-silent no-op ONCE so the operator knows the reload-backup did not take.
+ if(!_capSaveWarned){ _capSaveWarned=true; try{ toast('⚠ Could not back up this capture (storage full) — keep this tab open until you save the item'); }catch(e2){} }
+} }
function restoreCaptureState(){ try{
const s=sessionStorage.getItem(_CAP_KEY); if(!s) return false; const d=JSON.parse(s); if(!d) return false;
+ if(!_capFresh(d)){ clearCaptureState(); return false; } // TK-11962: never restore a stale (>2h) capture
_frontPhoto=d.front||null; _backPhoto=d.back||null; _media=Array.isArray(d.media)?d.media:[];
_addPhoto=d.addPhoto||_frontPhoto||_backPhoto||null; _idSource=d.idSource||null; _addExtracted=d.extracted||{};
if(d.fields) Object.keys(d.fields).forEach(id=>{ const el=$('#'+id); if(el && d.fields[id]!=null) el.value=d.fields[id]; });
← fc87163 auto-data-snapshot: 2026-09-21T06:39:23 (1 data files) — dat
·
back to Dw Photo Capture
·
auto-data-snapshot: 2026-09-21T10:59:35 (1 data files) — dat 10fd713 →