---
phase: 20.1-ui-polish-and-bug-fixes
plan: 04
subsystem: ui, sync
tags: [rust, slint, shopify, product-images, sqlite, image-cache]

# Dependency graph
requires:
  - phase: 20.1-01
    provides: Inter font, UI polish, note/item placeholders
  - phase: 20.1-02
    provides: 44px avatar ring, RecipientGrid avatar treatment
  - phase: 20.1-03
    provides: SC8 sidecar hide, SC11-SC17 fixes
provides:
  - SC18: Shopify product image download pipeline fixed with local cache and diagnostic logging
  - SC19: Product images display in card item squares, ProductGrid tiles, and product detail sidecar
affects: [card-item-squares, product-grid, product-detail-sidecar, sync-cycle]

# Tech stack
added:
  - crates/app/src/product_image_cache.rs (new module: download_product_image, get_cached_product_image_path)
patterns:
  - Local image cache at %APPDATA%/WITwhat/product-images/{product_id}.{ext}
  - Same pattern as Discord avatar cache (dirs::data_dir() + WITwhat subdirectory)
  - slint::Image::load_from_path for cached-file-to-Slint bridge
  - https-only validation before downloading (T-20.1-09)

# Key files
created:
  - crates/app/src/product_image_cache.rs
modified:
  - crates/app/src/lib.rs (added product_image_cache mod)
  - crates/app/src/live_client.rs (diagnostic eprintln! + download_product_image call in sync_products)
  - crates/app/src/main.rs (image field population in 4 build sites + product detail sidecar setter)
  - crates/app/ui/card.slint (image field on ItemSquareData + if sq.has-image Image branch)
  - .planning/DATA-FLOW.md (image field documented on ItemSquareData per RULE-06)

# Decisions
- product_image_cache placed in app crate lib (accessible from both live_client.rs and main.rs via crate::)
- https-only URL validation before any download (T-20.1-09 mitigation)
- Cache check before download in get_cached_product_image_path (stat before network)
- image field populated in build_item_squares_from_vm via SQLite product row lookup (not passed in vm)
- has_image derived from whether slint::Image::load_from_path succeeded (not just image_url.is_some())
- image-fit: contain for letterbox display (D-06: colored background shows around image edges)

# Metrics
duration: ~35min
completed: 2026-04-12
tasks_completed: 2
files_changed: 6
---

# Phase 20.1 Plan 04: Product Image Download Pipeline and UI Wiring Summary

**One-liner:** Shopify CDN image download pipeline with local file cache and Slint `image-fit: contain` display in card squares, ProductGrid tiles, and product detail sidecar.

## What Was Built

### Task 1: Product Image Cache Module + Shopify Pipeline Diagnostics (SC18)

**New module** `crates/app/src/product_image_cache.rs`:
- `download_product_image(url, product_id) -> Option<PathBuf>`: downloads CDN image to `%APPDATA%/WITwhat/product-images/{product_id}.{ext}`, skips if already cached, rejects non-https URLs
- `get_cached_product_image_path(product_id, image_url) -> Option<PathBuf>`: fast-path cache stat before attempting download
- `product_image_cache_dir()` / `product_image_cache_path()`: path helpers

**`live_client.rs` sync_products diagnostic logging** at 7 stages:
1. `shopify_client is_some=` guard check
2. `products_needing_image:` count
3. `shopify_url=` per-product
4. `extracted product_id=` after URL parse
5. `fetching image for product` before HTTP call
6. `image fetch status:` / `image fetch error:` after response
7. `image_src=` after JSON parse

**Download call** added after successful CDN URL extraction — `download_product_image` called before SQLite upsert so local cache is populated during sync.

### Task 2: UI Image Wiring (SC19)

**`card.slint`**: Added `image: image` field to `ItemSquareData` struct. Added `if sq.has-image : Image { source: sq.image; image-fit: contain; }` branch inside the clipped item square Rectangle (colored background shows around letterboxed edges per D-06).

**`main.rs`** — 4 build sites updated to populate `image` field:
- `build_item_squares_from_str_slice`: loads via `get_cached_product_image_path(name, img_url)` when `has_img=true`
- `build_item_squares_from_vm`: looks up `product.image_url` from SQLite `read_product_by_id`, loads via cache helper, derives `has_image` from whether load succeeded
- `sendable_to_product_tile_data`: loads from `product_id` + `image_hint` CDN URL (previously always `Default::default()`)
- `refresh_product_grid`: same pattern for SQLite-sourced product tiles

**Product detail sidecar**: `on_tile_clicked` handler now calls `set_detail_product_image(slint_image)` alongside existing `set_detail_has_image`. The `detail-product-image` and `detail-has-image` properties were already declared in `dashboard.slint` and wired to `ProductDetailPanel`.

**`DATA-FLOW.md`**: `ItemSquareData Field Mapping` table updated with `image` field row per RULE-06.

## Deviations from Plan

### Auto-fixed Issues

None — plan executed as written. The Slint-side wiring for `option-grid.slint` (`ProductTileData.image` field and `if tile-data.has-image : Image` branch) and `product-detail.slint` (`product-image` property + Image element) were already in place from prior phases. No Slint changes needed in those files.

## Known Stubs

None. All image loading sites are fully wired. Products without a local cached image correctly fall back to `Default::default()` (empty image), which keeps `has_image=false` and shows the colored initials/placeholder square instead.

## Threat Flags

| Flag | File | Description |
|------|------|-------------|
| threat_flag: external-download | crates/app/src/product_image_cache.rs | Downloads from Shopify CDN (https-only enforced, T-20.1-09 mitigated) |

## Self-Check

**Created files:**
- `crates/app/src/product_image_cache.rs` — FOUND (committed at 8527999)

**Key commits:**
- `8527999` feat(20.1-04): product image cache module and Shopify pipeline diagnostics (SC18)
- `a78e0f4` feat(20.1-04): wire product image display in card squares, ProductGrid tiles, and detail sidecar (SC19)

**Build:** `cargo build -p app` compiles without errors (binary replacement blocked by running process, library compiles clean).

## Self-Check: PASSED
