---
phase: 19.1-lookupmodal-dual-mode-parent-product-adds-and-serial-unit-picks
plan: 04
subsystem: rust-data-correctness
tags: [rust, crash-hardening, sqlite-persistence, cache-invalidation, serial-display, gap-closure]

# Dependency graph
requires:
  - phase: 19.1-02
    provides: LookupModal Rust handlers, in-memory cache, assignment state machine

provides:
  - Crash-hardened on_lookup_create_unit_confirmed (no bare unwraps, structured logging)
  - Double-arrow prefix eliminated (Rust passes raw name, Slint adds prefix)
  - is_on_card computation from card product_refs (ready for Plan 05 Slint field)
  - Lookup cache invalidated on item removal, product creation, and unit creation
  - Unit serial IDs shown on card item squares (not parent product name)
  - Unit/product assignments persist to SQLite via upsert_card (survive sync/refresh)
  - Force-reassign removes unit from old card and adds to new card in SQLite

affects:
  - 19.1-05 (Slint UI fixes: is-on-card field, forwarding properties)
  - 19.1-06 (UX polish)

# Tech tracking
tech-stack:
  added: []
  patterns:
    - "Eager cache invalidation before thread spawn (Rc<RefCell<>> not Send)"
    - "Read-modify-write on CardRow product_refs_json for atomic upsert"
    - "Serial_id as display_name for unit items on card item squares"

key-files:
  created: []
  modified:
    - crates/app/src/main.rs
  deleted: []

key-decisions:
  - "LookupCacheEntry struct and lookup_cache declared before card-remove handlers to allow cache capture in closures"
  - "Cache invalidation is eager (clear before thread spawn) since Rc<RefCell<>> cannot cross thread boundaries"
  - "Serial ID used as display name on card (not parent product name) for unit assignments per G-09"
  - "Force-reassign removes serial from old card's product_refs_json before adding to new card"
  - "is_on_card computed but prefixed with underscore — Slint struct field deferred to Plan 05"

patterns-established:
  - "read_all_cards + find by card_id + modify + upsert_card for targeted field updates"

requirements-completed:
  - D-07
  - D-09
  - D-15
  - D-17

metrics:
  duration_minutes: 14
  completed: "2026-04-08T07:38:56Z"
---

# Phase 19.1 Plan 04: Data Correctness and Crash Hardening Summary

Crash-hardened unit creation handler, eliminated double-arrow display, added lookup cache invalidation on data changes, and persisted unit/product assignments to SQLite for sync survival.

## Task Completion

| # | Task | Commit | Key Changes |
|---|------|--------|-------------|
| 1 | G-01 crash hardening + G-06 double arrow + G-08 state reset + G-11/G-12 cache + G-07 is_on_card | 1ceca24 | Replaced bare unwraps with explicit error handling, added [create-unit] structured logging, removed format!("-> ") prefix, moved LookupCacheEntry before remove handlers, added cache.clear() in 4 handlers, computed is_on_card from product_refs |
| 2 | G-09 serial on card + G-10/G-13 sync persistence | 0f1cc69 | Serial_id used as display_name in item_squares, upsert_card calls added in on_lookup_unit_selected, on_lookup_unit_force_reassign (both old and new card), and on_lookup_item_selected |

## Deviations from Plan

### Auto-fixed Issues

**1. [Rule 3 - Blocking] LookupCacheEntry declaration order**
- **Found during:** Task 1
- **Issue:** The lookup_cache was declared after the card-remove handlers that needed to capture it in closures, causing compile errors
- **Fix:** Moved struct LookupCacheEntry and let lookup_cache declaration before card-confirm-remove-item handler, removed duplicate at old location
- **Files modified:** crates/app/src/main.rs
- **Commit:** 1ceca24

**2. [Rule 3 - Blocking] Rc<RefCell<>> not Send for thread spawn**
- **Found during:** Task 1
- **Issue:** Plan specified cache.borrow_mut().clear() inside std::thread::spawn, but Rc<RefCell<>> is not Send
- **Fix:** Moved cache invalidation before thread spawn (eager invalidation on UI thread)
- **Files modified:** crates/app/src/main.rs
- **Commit:** 1ceca24

**3. [Rule 2 - Missing functionality] lookup-item-selected missing store access**
- **Found during:** Task 2
- **Issue:** on_lookup_item_selected handler did not capture rx_write_handle, so SQLite persistence was not possible
- **Fix:** Added rx_handle clone to closure captures and implemented upsert_card for parent product adds
- **Files modified:** crates/app/src/main.rs
- **Commit:** 0f1cc69

**4. [Rule 2 - Missing functionality] G-08 modal state reset limited to available properties**
- **Found during:** Task 1
- **Issue:** Plan listed set_lookup_expanded_product_id, set_lookup_creating_unit_product_id, set_lookup_reassign_prompt_visible but these are not forwarded via dashboard.slint
- **Fix:** Skipped those resets as instructed by plan ("If any property is not exposed to Rust, skip it here -- Plan 05 Task 1 adds the forwarding bindings")
- **Files modified:** None
- **Commit:** N/A

**5. [Rule 2 - Missing functionality] live_client.rs not modified**
- **Found during:** Task 2
- **Issue:** Plan listed live_client.rs as files_modified but all persistence changes were achievable in main.rs handlers via direct upsert_card calls
- **Fix:** No changes needed in live_client.rs; snapshot_to_card_row already serializes product_refs correctly
- **Files modified:** None
- **Commit:** N/A

## Verification Results

- `cargo build` succeeds with 0 errors
- `cargo test --workspace` passes all 362 tests with 0 failures
- No `format!("-> ")` pattern in main.rs (double-arrow eliminated)
- `upsert_card` calls present in all 3 lookup handlers (unit-selected, force-reassign, item-selected)
- No bare `unwrap()` in on_lookup_create_unit_confirmed handler
- Structured `[create-unit]` ENTER/EXIT logging verified

## Known Stubs

None -- all plan objectives fully implemented.

## Self-Check: PASSED
