Background
Split out from the descoped #115 Phase D. Phase A + B.1 of the diff viewer have shipped; this is the remaining "promote screenshot comparison to a first-class view" item.
Current state
- The Watch pipeline already captures Playwright screenshots (
src/core/screenshot.py) and computes a Change.visual_change_score (perceptual similarity).
- The Change Detail page renders a side-by-side "Visual Comparison" block with the two raw screenshots (
pages/change_detail.html near line 49) when both snapshots have screenshot_path set.
- That block is informational — there's no overlay, no slider, no diff highlighting. Users see "Before" and "After" but have to eyeball the differences themselves, which is impractical for large pages.
Goal
Promote the Visual Comparison from "two thumbnails" to a real visual diff tool:
- Slider mode — single image area with a draggable vertical handle; left of handle shows Before, right shows After. Standard before/after slider pattern.
- Overlay mode — Before + After stacked, with a per-pixel difference mask highlighted in magenta or similar high-contrast color. Useful for spotting subtle changes a slider misses.
- Mode toggle — a
segment-group like the existing diff modes: Side-by-side (current), Slider, Overlay.
- Optional: zoom/pan — for high-resolution screenshots, click-to-zoom.
Approach sketch
- Pixel-diff computation: Pillow already pulled in (or via
pillow extra). Use ImageChops.difference + a threshold to produce an alpha-masked overlay PNG. Compute server-side once, cache to storage alongside the snapshots; serve via a new route /watches/{id}/screenshot-diff?from={prev_id}&to={curr_id}.
- Slider (client-side): vanilla JS + CSS
clip-path on the After image, wired to a draggable handle. ~50 lines, no JS dep.
- Overlay (client-side): just an
<img> with the precomputed diff PNG layered over the After.
- Mode switching: Alpine or plain JS toggling visibility of the three modes.
Acceptance
Open design questions
- Where does the diff PNG live?
LocalStorage under a new screenshots-diff/{change_id}.png path, computed lazily on first view? Or eagerly during the check pipeline?
- Threshold tuning — anti-aliasing differences shouldn't show; need a perceptual threshold, not bitwise.
- Long pages — Playwright captures full-page screenshots; some watches produce 10000+ px tall images. Slider/overlay UX needs to handle that (scroll within the visual area).
Dependencies
- Reuses existing screenshot capture pipeline.
- New dep: none (Pillow likely sufficient; if perceptual diff needs more, consider
imagehash or numpy — defer until measurement says we need them).
Related
Background
Split out from the descoped #115 Phase D. Phase A + B.1 of the diff viewer have shipped; this is the remaining "promote screenshot comparison to a first-class view" item.
Current state
src/core/screenshot.py) and computes aChange.visual_change_score(perceptual similarity).pages/change_detail.htmlnear line 49) when both snapshots havescreenshot_pathset.Goal
Promote the Visual Comparison from "two thumbnails" to a real visual diff tool:
segment-grouplike the existing diff modes: Side-by-side (current), Slider, Overlay.Approach sketch
pillowextra). UseImageChops.difference+ a threshold to produce an alpha-masked overlay PNG. Compute server-side once, cache to storage alongside the snapshots; serve via a new route/watches/{id}/screenshot-diff?from={prev_id}&to={curr_id}.clip-pathon the After image, wired to a draggable handle. ~50 lines, no JS dep.<img>with the precomputed diff PNG layered over the After.Acceptance
aria-labeldescribes what's shownscreenshot_path, the toggle is hidden (current behavior)Open design questions
LocalStorageunder a newscreenshots-diff/{change_id}.pngpath, computed lazily on first view? Or eagerly during the check pipeline?Dependencies
imagehashornumpy— defer until measurement says we need them).Related
Change.visual_change_scorealready computed; this issue makes it actionable for users