# Phase 20.1: UI Polish and Bug Fixes - Research

**Researched:** 2026-04-12
**Domain:** Slint UI, Rust/Slint data wiring, GitHub Issues client, SQLite, Inter font embedding
**Confidence:** HIGH

---

<user_constraints>
## User Constraints (from CONTEXT.md)

### Locked Decisions

- **D-01:** Avatar size is 40px inner circle + 2px purpose-colored ring = 44px total outer diameter. Unified across card tiles AND Recipients option grid tiles.
- **D-02:** No subtext repositioning. Grow recipient name and contact-secondary font sizes to proportionally fill the additional vertical space created by the larger avatar (up from 30px to 44px).
- **D-03:** Recipients option grid tiles get full card-style avatar treatment: 44px avatar with purpose-colored ring, real Discord avatar image when available, initials fallback, tooltip on hover. Tile height grows to accommodate.
- **D-04:** Bundle Inter font with the application binary. Inter replaces Slint's default system font for all text rendering across the entire app.
- **D-05:** All existing Typography scale sizes (11px, 12px, 13px, 18px) remain as-is. The font change alone should fix vertical alignment inconsistencies.
- **D-06:** Card item squares use `image-fit: contain` (letterboxed) when a product image is available. Existing colored background shows around edges.
- **D-07:** Debug and fix the Shopify image auto-fetch pipeline in `live_client.rs` (sync_products, lines ~1196-1258). Bug fix, not new logic.
- **D-08:** Wire the missing Image element in `card.slint` item squares. Only the `!sq.has-image` branch renders; add the `sq.has-image` branch with an Image element.
- **D-09:** Per-state contextual messages in dashboard empty state:
  - State 0 (Not configured): "Open Settings to connect your GitHub project" (keep existing)
  - State 1 (Connecting): "Syncing..."
  - State 2 (Connected): "No orders to show"
  - State 4 (Connected, no Shopify): "Connect Shopify to see orders"

### Claude's Discretion

- Exact font size increases for recipient name and contact-secondary to fill the avatar space (D-02)
- Inter font weight variants to bundle (Regular 400, Medium 500, SemiBold 600, Bold 700 recommended)
- How to load image-url string into a Slint Image source in card item squares
- Root cause diagnosis of Shopify image download failure
- All remaining 10 success criteria (SC2, SC3, SC8-16, SC17) are specific bug fixes with clear expected behavior — no design decisions needed

### Deferred Ideas (OUT OF SCOPE)

None — discussion stayed within phase scope.

</user_constraints>

---

## Summary

Phase 20.1 is a multi-domain polish and bug fix phase covering 19 specific success criteria. The work divides cleanly into four clusters: (1) avatar visual refresh in `card.slint` and `option-grid.slint`, (2) font strategy — bundling Inter via Slint's font embedding API, (3) product image pipeline — fixing Shopify auto-fetch and wiring the Image element in card squares, and (4) a collection of data-integrity and behavioral bug fixes spread across `main.rs`, `live_client.rs`, the GH Issues client, and the state transition modal.

The codebase is well-structured for these changes. The avatar ring pattern already exists in `card.slint` (lines 298–336) and only needs dimension scaling and replication to `option-grid.slint`. The `ItemSquareData` struct already has `has-image` and `image-url` fields — only the Slint Image element rendering branch is missing. The Shopify image fetch code exists in `live_client.rs` but appears to fail silently at the URL parsing or HTTP stage. The GH Issues creation pattern for lookup-modal product creates is directly reusable from `on_on_add_product_submit`.

The most risk-bearing items are: Inter font bundling (new territory for this codebase — no prior font embedding), Shopify image download root cause (unknown failure mode), and the `assigned_card_id` data integrity fix (requires tracing the card_id value from Slint callback through to SQLite and GH Issue write).

**Primary recommendation:** Sequence the phase as: font first (isolated, high-value, no regressions), then avatar scaling, then image wiring, then data-integrity bug fixes, then the remaining UI polish items.

---

## Standard Stack

### Core (already in use — no new dependencies needed for most items)

| Library | Version | Purpose | Why Standard |
|---------|---------|---------|--------------|
| slint | 1.15.1 [VERIFIED: Cargo.lock] | UI framework — all Slint components | Already in use |
| slint-build | 1.x [VERIFIED: Cargo.toml] | Build-time Slint compilation | Already in use |
| serde_json | 1.x [VERIFIED: Cargo.toml] | JSON serialization for GH Issue bodies | Already in use |
| ureq | 2.x [VERIFIED: Cargo.toml] | HTTP client for Shopify image fetch | Already in use |

### New Dependency (Inter font files only)

No new Rust crates needed. Inter font files (.ttf) must be added to the source tree and referenced via Slint's font embedding mechanism.

**Inter font source:** https://fonts.google.com/specimen/Inter or https://github.com/rsms/inter/releases [ASSUMED — canonical location, not verified in session]

**Weights to bundle (D-04, discretion):** Regular 400, Medium 500, SemiBold 600, Bold 700. These four cover all existing text uses in the app.

---

## Architecture Patterns

### Pattern 1: Slint Font Embedding

**What:** Slint 1.x supports custom font registration at app startup using `slint::platform::register_font_from_data()` or via the Slint `.slint` file with `@font-face`. The preferred approach for bundling is embedding font bytes via `include_bytes!()` in Rust and calling the registration API before the window is created. [ASSUMED — based on training knowledge of Slint 1.x; must verify against Slint 1.15 docs before implementation]

**When to use:** Before `DashboardWindow::new()` in `main.rs`. Font registration must precede window construction.

**Pattern (verify against Slint 1.15 docs before coding):**
```rust
// main.rs — before DashboardWindow::new()
// Option A: Rust-side registration
slint::platform::register_font_from_data(
    include_bytes!("../ui/fonts/Inter-Regular.ttf"),
    None
).expect("failed to register Inter Regular");
```

Alternatively, Slint `.slint` files support `@font-face { src: url("fonts/Inter-Regular.ttf"); }` which the build pipeline embeds at compile time. This is cleaner as it requires no Rust code change. [ASSUMED — verify in Slint 1.15 docs]

**Font file location:** `crates/app/ui/fonts/` (create this directory). The build system already handles `ui/` as a Slint resource root.

### Pattern 2: Avatar Ring Scaling (card.slint → option-grid.slint)

**What:** The existing avatar ring in `card.slint` lines 298–336 uses a 30px outer Rectangle (border-radius 15px) with a 26px inner Rectangle (x:2, y:2, border-radius 13px). Scaling to 44px outer / 40px inner means: outer Rectangle 44×44 border-radius 22px, inner Rectangle 40×40 (x:2, y:2) border-radius 20px.

**Verified dimensions from CONTEXT.md D-01:** 40px inner circle + 2px purpose-colored ring = 44px total outer diameter. [VERIFIED: CONTEXT.md]

**card.slint change:**
```slint
// Before (lines 298-336):
Rectangle {
    width: 30px; height: 30px; border-radius: 15px;
    Rectangle { x: 2px; y: 2px; width: 26px; height: 26px; border-radius: 13px; }
}

// After:
Rectangle {
    width: 44px; height: 44px; border-radius: 22px;
    Rectangle { x: 2px; y: 2px; width: 40px; height: 40px; border-radius: 20px;
        if root.has-avatar-image : Image { width: 40px; height: 40px; ... }
        if !root.has-avatar-image : Text { font-size: Typography.size-sm; ... }
    }
}
```

**option-grid.slint RecipientTileData struct change:** Needs new fields for avatar support:
- `has-avatar-image: bool`
- `avatar-image: image`
- `purpose-color: color`

The `RecipientGrid` tile in `option-grid.slint` currently uses a plain 24px circle with no image support (lines 80–95). Full card-style avatar treatment is a structural addition, not a resize.

**Rust wiring:** `build_recipient_tiles()` in `main.rs` (line 765+) builds `RecipientTileData`. It currently only populates `name`, `initial`, `section_header`, `grid_row`, `grid_col`, `dimmed`, `contact_secondary`. Avatar fields must be added: resolve `avatar_image_path` from the `DashboardCardViewModel` for each recipient name.

### Pattern 3: Item Square Image Rendering

**What:** `card.slint` item square area (lines 440–480) renders only the `!sq.has-image` (initials) branch. The `sq.has-image` branch with an actual Image element is missing. The Slint `Image` element requires an `image` type source, not a URL string.

**Image loading pattern (from existing avatar code in main.rs line 1227):**
```rust
// Already used for avatars:
slint::Image::load_from_path(std::path::Path::new(path)).ok()
```

Product images stored as CDN URLs (not local paths) require downloading to a local cache first, then loading from path. The avatar pipeline uses `%APPDATA%/WITwhat/avatars/` as the cache directory. Product images should use `%APPDATA%/WITwhat/product-images/` analogously.

**Slint `image-fit: contain` syntax:** [ASSUMED — standard Slint Image property; verify in Slint 1.15 docs]

**card.slint addition (inside clipped inner Rectangle):**
```slint
if sq.has-image : Image {
    source: sq.image;   // NOTE: requires image type, not string
    width: parent.width;
    height: parent.height;
    image-fit: contain;
}
```

This means `ItemSquareData` in `card.slint` needs an `image: image` field added alongside the existing `image-url: string` field. The Rust `view_model_to_card_data` builds `ItemSquareData` — it will need to load the image from disk at build time.

**Alternative approach:** If CDN URLs are stored in `image_url`, download and cache them to disk during the view model build, then load from local path. This is equivalent to the avatar pipeline (two-tier: CDN → disk → Slint Image).

### Pattern 4: GH Issue Creation on Lookup-Modal Product Create

**What:** `on_lookup_create_confirmed` in `main.rs` (line 3935) creates a product in SQLite and dispatches an `EditCommand::AddItem` but does NOT create a `ww-product` GH Issue. The pattern to fix this is directly reusable from `on_on_add_product_submit` (line 4056–4146), which already creates the GH Issue on a background thread and then updates the SQLite row with the issue number.

**Key difference:** `on_lookup_create_confirmed` is called with a `card_idx` (the card the item is being added to), while `on_on_add_product_submit` is called from the product catalog form. The GH Issue creation code is identical; only the trigger site differs.

**Fix approach:** After writing the product to SQLite inside `on_lookup_create_confirmed`, spawn a background thread using the same `GhIssuesClient::new()` + `client.create_issue(name, body, "ww-product")` pattern. The `issues_client::format_issue_body_json` helper is already importable.

**Important:** The product_id must be a UUID. Currently `on_lookup_create_confirmed` does NOT generate a product_id — it uses `display_name` as the item identifier. Before adding GH Issue creation, the product must first be upserted with a proper UUID product_id. [ASSUMED from code reading — verify in implementation]

### Pattern 5: Shopify Image Download Root Cause

**What:** `live_client.rs` lines 1196–1258 contain the Shopify image auto-fetch. The code:
1. Filters products with `shopify_product_url.is_some() && image_url.is_none()` [VERIFIED: code]
2. Parses the numeric Shopify product ID from the URL via `rsplit('/')` [VERIFIED: code]
3. Calls `GET /admin/api/2024-01/products/{id}.json?fields=images` via ureq [VERIFIED: code]
4. Parses `json.pointer("/product/images/0/src")` [VERIFIED: code]
5. Updates SQLite and the GH Issue body [VERIFIED: code]

**Possible failure modes:**
- The Shopify client passed to `sync_products` may be `None` (Shopify not configured). The outer `if let Some(shopify) = shopify_client` guard would skip the block entirely with no log output. [ASSUMED based on code pattern — verify by adding diagnostic logging]
- The URL parsing (`rsplit('/')` → filter only ASCII digits) may fail if Shopify URLs contain query parameters or non-numeric segments. The code does strip `?` but other edge cases may exist.
- The `ureq::get` call may be failing due to a missing HTTP client configuration, authentication, or rate limiting with no retry.
- The `update_product_issue_image` call at line 1273 may panic or fail if `issues_client` is not initialized.

**D-07 fix approach:** Add `eprintln!` diagnostics at each stage (shopify_client is None? URL parse failed? HTTP error? JSON parse error?) and run a sync cycle with Shopify configured to collect the error output.

### Pattern 6: assigned_card_id Bug Fix

**What:** The `assigned_card_id` stored in the GH Issue body (for `ww-unit` issues) stores the card display name instead of the unique `card_id` UUID. The bug is in `main.rs` where `unit-selected` callback is wired (around line 4200+ based on context). The `card_data.package_id` is the correct unique identifier.

**Fix:** In `on_unit_selected` callback, pass `card_data.package_id.to_string()` (not `card_data.recipient_name.to_string()`) as the `card_id` argument to `try_assign_unit`. The todo notes the trace starts at the `unit-selected` Slint callback.

### Pattern 7: State Change Preserves Card Assignment

**What:** `on_state_modal_confirmed` in `main.rs` (lines 4577–4672) has:
```rust
let effective_unassign = unassign || new_state_str == "Available";
```
This correctly auto-unassigns when Available is selected (G-15). But the `update_unit_state` call at line 4632 reads `existing_card_id` only when `!effective_unassign`. The issue described in the todo ("state change unassigns unit even without checkbox") may mean `effective_unassign` is true in cases where it shouldn't be, or `existing_card_id` is not being preserved correctly.

**Fix scope:** Verify the `effective_unassign` logic is correct (Available state → force unassign is correct per G-15). If the bug is that states other than Available are triggering unassign, check whether the `unassign` boolean passed from Slint is unexpectedly `true`. The `unassign-checked` property in `state-transition-modal.slint` is `in-out` — verify it is reset to `false` when the modal is opened.

### Pattern 8: Product Detail Sidecar Hides on Tab Switch (SC8)

**What:** `restore_mode_state` in `main.rs` (line 1117) does NOT call `w.set_product_detail_visible(false)`. When switching tabs, the sidecar persists. The fix is a one-liner: add `w.set_product_detail_visible(false)` inside `on_tab_clicked` before `restore_mode_state`, or inside `restore_mode_state` itself (unless a mode should restore sidecar state, which none currently do).

**Verified:** `on_tab_clicked` (line 3069) calls `restore_mode_state` but never clears `product_detail_visible`. [VERIFIED: code]

### Anti-Patterns to Avoid

- **Slint `image` type vs `string` type:** Never pass a URL string directly to a Slint `Image.source`. Images must be loaded via `slint::Image::load_from_path()` from a local file. Passing a URL string will not work.
- **INSERT OR REPLACE for product upserts:** Per `SQLITE_TIPS.md`, use `ON CONFLICT DO UPDATE SET` to avoid wiping `github_issue_number` when the sync cycle upserts products again. [VERIFIED: code_tips/SQLITE_TIPS.md]
- **avatar_image_path in RecipientTileData:** The field does not exist on `RecipientTileData` struct currently. It must be added to both the Slint struct and the Rust builder function, not assumed.

---

## Don't Hand-Roll

| Problem | Don't Build | Use Instead | Why |
|---------|-------------|-------------|-----|
| Font embedding | Custom font loader | Slint's `@font-face` or `register_font_from_data()` | Framework provides this; hand-rolled would require platform-specific GDI/DirectWrite calls |
| Image download + cache | Custom HTTP + disk cache | ureq (already in use) + local file path pattern (same as avatar pipeline) | Consistent with existing avatar download architecture |
| GH Issue JSON body format | Custom serializer | `integrations::github::issues_client::format_issue_body_json()` | Already used by `on_on_add_product_submit`; reuse directly |
| UUID generation | Random string | `uuid::Uuid::new_v4()` (already in use) | Already imported via `uuid` crate |

---

## Common Pitfalls

### Pitfall 1: Slint Image Type Mismatch
**What goes wrong:** Adding `image-url: string` to `ItemSquareData` and trying to bind it to `Image.source` directly causes a Slint type error. `Image.source` requires the `image` type, not `string`.
**Why it happens:** Slint distinguishes `string` and `image` as distinct types. URL strings cannot be used as image sources.
**How to avoid:** Add a separate `image: image` field to `ItemSquareData`. Populate it from Rust using `slint::Image::load_from_path()` on the cached local file. `image-url` can remain as the source-of-truth string for the path.
**Warning signs:** Slint compile error at build time: "type mismatch, expected image".

### Pitfall 2: Product Images Are CDN URLs, Not Local Paths
**What goes wrong:** `product.image_url` in SQLite stores a Shopify CDN URL (e.g., `https://cdn.shopify.com/...`), not a local file path. Passing this URL to `slint::Image::load_from_path()` will fail.
**Why it happens:** Per Phase 17 decision: "Shopify image: CDN URL stored directly as image_url for simplicity."
**How to avoid:** Implement a local image cache: download the CDN URL via ureq, save to `%APPDATA%/WITwhat/product-images/{product_id}.jpg`, then load from that path. Check cache before downloading.
**Warning signs:** `load_from_path` returns `Err` at runtime; images appear blank.

### Pitfall 3: RecipientTileData Missing Avatar Fields
**What goes wrong:** Adding avatar ring display to `option-grid.slint` RecipientGrid tiles but not updating the `RecipientTileData` struct causes a Slint compile error.
**Why it happens:** `RecipientTileData` is defined in `option-grid.slint` and its Rust counterpart is generated by slint-build. Any new fields must be added to both the Slint struct and populated in the Rust builder.
**How to avoid:** Add `has-avatar-image: bool`, `avatar-image: image`, `purpose-color: color` to `RecipientTileData` in `option-grid.slint` AND populate them in `build_recipient_tiles()` in `main.rs` using the same `slint::Image::load_from_path` pattern as `view_model_to_card_data`.
**Warning signs:** Slint compile error on struct field access; Rust borrow checker error on avatar_image_path.

### Pitfall 4: INSERT OR REPLACE Wiping github_issue_number
**What goes wrong:** After creating a product via the lookup modal and getting a GH Issue number, the next sync cycle's `upsert_product` with an `INSERT OR REPLACE` pattern wipes the `github_issue_number` column.
**Why it happens:** Per `SQLITE_TIPS.md`: "INSERT OR REPLACE is a DELETE + INSERT — any columns not in the INSERT list reset to NULL."
**How to avoid:** Use `ON CONFLICT(product_id) DO UPDATE SET ... WHERE github_issue_number IS NULL` or the pattern already used by `set_card_issue_number`. Verify the product upsert SQL uses proper conflict resolution.
**Warning signs:** Product loses its GH issue link after next sync; "ww-product issue created" but product still gets deleted on sync because `github_issue_number` is NULL again.

### Pitfall 5: State Modal Unassign Checkbox Not Reset on Open
**What goes wrong:** `unassign-checked` is `in-out` on `StateTransitionModal`. If it retains `true` from a previous modal open, the next state change will also unassign even without the user checking the box.
**Why it happens:** Slint `in-out` properties on components persist between renders. If the modal is not torn down and rebuilt, the checkbox state carries over.
**How to avoid:** Reset `w.set_state_modal_unassign_checked(false)` (if such a setter exists) or set `unassign-checked = false` in Slint when `close-requested` fires. Verify this reset happens at every modal open site in `main.rs`.

### Pitfall 6: Font Embedding Build Order
**What goes wrong:** Calling `slint::platform::register_font_from_data()` after `DashboardWindow::new()` has no effect — the font is not used.
**Why it happens:** Slint's rendering backend initializes its font stack on window creation.
**How to avoid:** All font registration calls must come BEFORE the window is created in `main.rs`.

### Pitfall 7: Lookup Modal Creates Product But Not as Catalog Entry
**What goes wrong:** `on_lookup_create_confirmed` currently uses `display_name` as an ad-hoc item identifier (not a UUID). If GH Issue creation is added without first properly upsert-ing the product with a UUID product_id, the unit assignment will reference an inconsistent product.
**Why it happens:** The lookup modal's create flow was designed for "add this named item" not "create a catalog product". The two flows are now expected to be equivalent (SC17).
**How to avoid:** In the fix for SC17, generate a UUID product_id, write the product to SQLite first (same pattern as `on_on_add_product_submit`), then create the GH Issue in background. The card's product_refs should reference the proper product_id.

---

## Code Examples

### SC9: Serial Unit Search Box Seeds New Unit SN Field

Current behavior: The unit-search-query in `product-detail.slint` is a search filter. The [+] button (line 220–242) calls `create-unit-confirmed(product-id, serial-id)` but `serial-id` is taken from `new-serial-input` (which requires the user to type separately).

**Fix:** When the [+] button is clicked, pre-populate `new-serial-input` from `unit-search-query`:
```slint
// In [+] button TouchArea.clicked:
if !root.creation-in-progress {
    root.creating-unit = true;
    root.new-serial-input = root.unit-search-query;  // seed from search
}
```

### SC10: Default Card Shipping State Shows 'No items added' When Empty

Current: `card.item_display_label` is `""` when there are no items (line 470 and 531 in main.rs). The label row in card.slint renders as blank.

**Fix (discretion — no Rust change needed):** In `card.slint`, the Row 3 Text element reads `root.item-display-label`. Change the text binding to:
```slint
text: root.item-display-label != "" ? root.item-display-label : "No items added";
color: root.item-display-label != "" ? Colors.text-muted : Colors.text-dim;
```

Or handle in Rust: set `card.item_display_label = "No items added".into()` when `count == 0` in `build_item_squares_from_vm`.

### SC2: Replace Missing Note Warning with (add note) Placeholder

Current: `missing_label` is set to "Missing note" in `view_model_to_card_data` (line 1169) and rendered as a warning-colored Text in card.slint Row 6 (line 633). The `note-preview` is empty when no note exists.

**Fix:** Replace the warning label with a placeholder in the note area. When `note-preview == ""` and `!editing-note`, render `"(add note)"` in a dim color instead of the empty space. Remove the `missing-label` warning text for the MissingNote state.

```slint
// In Row 5 note display (card.slint ~line 547):
text: root.note-preview != "" ? root.note-preview : "(add note)";
color: root.note-preview != "" ? Colors.text-dim : Colors.text-muted;
```

Also update `view_model_to_card_data` to not set `missing_label = "Missing note"` (or the Slint change makes it irrelevant).

### SC15: Serial Units Textbox Shows Caret Cursor on Hover

`product-detail.slint` search input (line 187): The wrapping `TouchArea` on line 215 has no `mouse-cursor` set. A `TextInput` itself shows the text cursor when focused, but the surrounding TouchArea shows the default pointer.

**Fix:** Add `mouse-cursor: text;` to the outer `TouchArea` that wraps the serial-search TextInput:
```slint
TouchArea {
    mouse-cursor: text;
    clicked => { serial-search.focus(); }
}
```

### SC16: Serial Units Search Results Top-Aligned, [+] Button Center-Aligned

In `product-detail.slint` HorizontalLayout (line 174), the search input has `horizontal-stretch: 1` and the [+] button is 24px wide. The [+] button's vertical alignment within the HorizontalLayout depends on the layout's `alignment` property. The button is 24×24 inside a layout where the search input is 28px tall.

**Fix:** Set `alignment: center` on the HorizontalLayout, and ensure the Flickable unit list uses `alignment: start` in its VerticalLayout.

---

## Runtime State Inventory

This phase does not involve renaming or migration. However, the `assigned_card_id` data integrity fix (SC11) may leave existing GH Issue bodies with stale card name values. This is a data quality issue, not a migration requirement — old values will be overwritten by the next state change on each unit.

| Category | Items Found | Action Required |
|----------|-------------|-----------------|
| Stored data | SQLite `product_units.assigned_card_id` may contain display names instead of IDs | Code fix only (new assignments use card_id); existing records corrected on next unit state change |
| Stored data | SQLite `products.image_url` contains CDN URLs (not local paths) | Download-and-cache pipeline needed before Slint can display |
| Live service config | None | — |
| OS-registered state | None | — |
| Secrets/env vars | None affected | — |
| Build artifacts | No new crate installs; font .ttf files added to source tree | `cargo build` will recompile after font addition |

---

## Environment Availability

| Dependency | Required By | Available | Version | Fallback |
|------------|------------|-----------|---------|----------|
| Rust / cargo | All compilation | Assumed available (project builds) | — | — |
| Inter font files (.ttf) | D-04 font bundling | Must download | — | System font (current behavior — not acceptable per D-04) |
| Shopify API access | SC18 image download | Requires token configured | — | Skip image fetch; show initials placeholder |
| GitHub CLI (gh) | SC17 ww-product issue creation | Assumed available (existing usage) | — | Fail with error log |

**Missing dependencies with no fallback:**
- Inter font .ttf files — must be manually downloaded and committed to `crates/app/ui/fonts/` before SC5 (font bundling) can be implemented.

**Missing dependencies with fallback:**
- Shopify API access — image download only runs when Shopify is configured; graceful skip already coded.

---

## Validation Architecture

The project has no detected test config files for the `app` crate (no pytest.ini, jest.config.*, or test/ directory pattern). `crates/app/src/` has no test files. The `assignment.rs` module has inline `#[cfg(test)]` unit tests. No `workflow.nyquist_validation` key detected in `.planning/config.json` — treating validation as enabled.

### Test Framework
| Property | Value |
|----------|-------|
| Framework | Rust built-in `cargo test` |
| Config file | No dedicated test config — inline `#[cfg(test)]` modules |
| Quick run command | `cargo test -p app --lib` |
| Full suite command | `cargo test --workspace` |

### Phase Requirements → Test Map

| Behavior | Test Type | Automated Command | Notes |
|----------|-----------|-------------------|-------|
| Inter font registers without panic | smoke | `cargo build -p app` (compile-time check) | Font API errors are panics at startup |
| Avatar ring scales to 44px | manual/visual | Slint screenshot via BUGSWEEPER | No unit test path |
| Image element renders product image | manual/visual | BUGSWEEPER `GET /api/ui/cards` + screenshot | No unit test path |
| assigned_card_id uses UUID | unit | `cargo test -p app --lib -- assignment` | Extend existing tests in assignment.rs |
| GH Issue created on lookup-modal create | integration/manual | Run app, create product via lookup modal, check GH Issues | Requires live GH token |
| State change preserves assignment | unit | `cargo test -p app --lib -- assignment` | Add regression test for non-Available states |
| Sidecar hides on tab switch | manual | BUGSWEEPER tab-clicked callback invocation + property check | `GET /api/ui/properties` to verify product-detail-visible=false |

### Wave 0 Gaps
- [ ] Extend `crates/app/src/dashboard/assignment.rs` tests to cover SC14 regression (state change preserves assignment when unassign unchecked)
- [ ] No product image loading tests exist — manual verification via BUGSWEEPER required

---

## Security Domain

This phase involves no new authentication flows, external service credentials, or user-facing input validation beyond what already exists. The GH Issue creation for SC17 reuses the existing `GhIssuesClient` pattern. No new ASVS categories introduced.

| ASVS Category | Applies | Notes |
|---------------|---------|-------|
| V5 Input Validation | Existing | Product name validation already in `on_on_add_product_submit`; replicate for SC17 path |
| V6 Cryptography | No | No new crypto |

---

## Assumptions Log

| # | Claim | Section | Risk if Wrong |
|---|-------|---------|---------------|
| A1 | Slint 1.15 supports `register_font_from_data()` or `@font-face` in .slint files for custom font embedding | Architecture Patterns / Pattern 1 | Would require different font approach; investigate Slint 1.15 release notes |
| A2 | `slint::platform::register_font_from_data()` must be called before window creation | Pitfall 6 | Font might not be applied; debug at startup |
| A3 | Inter font is available at https://fonts.google.com/specimen/Inter | Standard Stack | Canonical source — easily verifiable |
| A4 | `image-fit: contain` is valid Slint Image syntax in 1.15 | Code Examples | May have different property name; check Slint docs |
| A5 | `on_lookup_create_confirmed` does not generate a product_id UUID | Architecture Patterns / Pattern 4 | Fix scope may differ if UUID already present |
| A6 | `unassign-checked` persists between modal opens (not reset) | Pitfall 5 | May already reset correctly; verify at call sites in main.rs |
| A7 | The `is_dimmed` item on product images was the only blocking issue for image display in card squares | Architecture Patterns / Pattern 3 | Image pipeline may have additional gaps |

---

## Open Questions (RESOLVED)

1. **Slint 1.15 font embedding API** — RESOLVED: Plans provide both `slint_build::compile_with_config` font path approach AND `@font-face` .slint fallback. Executor will verify which approach Slint 1.15.1 supports at implementation time.

2. **Shopify image download actual failure mode** — RESOLVED: Plan 04 Task 1 adds diagnostic logging at each branch point (shopify_client presence, URL parse, HTTP response, JSON parse) to determine the failure mode before applying fix. Root cause will be determined at execution time.

3. **ProductTileData image loading in option-grid.slint** — RESOLVED: Use synchronous `slint::Image::load_from_path()` at tile build time (same pattern as avatar loading). Product images are already cached on disk from the Shopify download pipeline.

---

## Sources

### Primary (HIGH confidence — code verified in session)
- `crates/app/ui/card.slint` — avatar ring dimensions (lines 298–336), item squares (lines 440–480), note display (lines 543–637) [VERIFIED]
- `crates/app/ui/option-grid.slint` — RecipientTileData struct, RecipientGrid tile structure, ProductGrid image paths [VERIFIED]
- `crates/app/ui/tokens.slint` — Typography scale sizes, Colors globals [VERIFIED]
- `crates/app/ui/dashboard.slint` — connection-status property (line 104), empty state text (lines 641–658) [VERIFIED]
- `crates/app/ui/settings-modal.slint` — Shopify vs Discord token section layout (full file) [VERIFIED]
- `crates/app/ui/product-detail.slint` — unit search, [+] button, image display [VERIFIED]
- `crates/app/ui/state-transition-modal.slint` — unassign checkbox behavior [VERIFIED]
- `crates/app/src/main.rs` — avatar loading (line 1227), on_state_modal_confirmed (line 4577), on_lookup_create_confirmed (line 3935), on_on_add_product_submit (line 4056), on_tab_clicked (line 3069), restore_mode_state (line 1117) [VERIFIED]
- `crates/app/src/dashboard/assignment.rs` — try_assign_unit, force_assign_unit, remove_unit_from_card [VERIFIED]
- `crates/app/src/live_client.rs` — Shopify image auto-fetch (lines 1196–1258) [VERIFIED]
- `crates/app/Cargo.toml` — slint 1.x, ureq 2.x, uuid, rfd [VERIFIED]
- `Cargo.lock` — slint 1.15.1 [VERIFIED]
- `code_tips/SQLITE_TIPS.md` — INSERT OR REPLACE pitfall, upsert pattern [VERIFIED]
- `.planning/phases/20.1-ui-polish-and-bug-fixes/20.1-CONTEXT.md` — all locked decisions [VERIFIED]
- Todo files (6) — assigned_card_id, unit assignment, state change, ww-product issue, image download, image display [VERIFIED]

### Tertiary (LOW confidence — training knowledge only)
- Slint 1.15 font embedding API exact method name and call site requirements [ASSUMED — A1, A2]
- `image-fit: contain` as valid Slint Image property syntax [ASSUMED — A4]

---

## Metadata

**Confidence breakdown:**
- Standard stack: HIGH — all dependencies already in use; Inter font location is canonical
- Architecture: HIGH for Rust patterns (verified from code); MEDIUM for Slint font embedding (API assumed)
- Pitfalls: HIGH — derived from actual code reading and SQLITE_TIPS.md

**Research date:** 2026-04-12
**Valid until:** 2026-05-12 (stable codebase; Slint 1.15 API docs should be verified before font implementation)
