---
phase: 20.1.1-change-the-i-button-on-cards-to-show-a-scrollable-history-of
verified: 2026-04-15T12:00:00Z
status: human_needed
score: 20/20
overrides_applied: 0
decisions_verified: 20/20
human_verification:
  - test: "Open a card's notes popover and submit a note with Ctrl+Enter"
    expected: "Note appears at top of list instantly (optimistic); composer collapses"
    why_human: "Ctrl+Enter key event behavior in a running Slint PopupWindow cannot be exercised via grep/static analysis"
  - test: "Open the notes popover, type text, then press Esc"
    expected: "Composer collapses and draft is cleared; popover remains open"
    why_human: "Nested Esc handling (composer vs popup dismiss) requires live UI interaction to confirm priority"
  - test: "Click outside an open notes popover"
    expected: "Popover closes; no crash"
    why_human: "close-on-click-outside policy is a Slint runtime behavior not testable statically"
  - test: "On the dashboard, click a card's recipient name (top row)"
    expected: "App switches to Recipients tab (index 2); matching tile highlighted; recipient detail sidebar opens with correct data"
    why_human: "Three-step tab-navigate-then-populate (set_mode_by_index + invoke_tab_clicked + populate_sidebar) requires live navigation to confirm correct sequencing"
  - test: "In the Recipients tab, click a tile to open the sidebar; then click the X button"
    expected: "Sidebar closes; tile highlight remains"
    why_human: "Sidebar dismissal and editing-state reset require live UI interaction to confirm"
  - test: "In the Recipients tab sidebar, press Esc while editing a field, then Esc again"
    expected: "First Esc cancels the active inline edit; second Esc closes the sidebar"
    why_human: "Two-level Esc priority (cancel-edit vs dismiss) requires live interaction"
  - test: "Hover over a note row's timestamp in the notes popover"
    expected: "Absolute ISO date appears on hover (opacity transitions from 0 to 1)"
    why_human: "Hover-reveal opacity behavior requires live UI interaction"
---

# Phase 20.1.1: Notes History Popover + Recipient Detail Sidebar — Verification Report

**Phase Goal:** Repurpose the card (i) button to open a scrollable ww-note history popover with inline composer; remove note row from card face and enlarge product squares; add a recipient-detail sidebar in the Recipients tab.
**Verified:** 2026-04-15T12:00:00Z
**Status:** human_needed (all 20 decisions verified in code; 7 live-UI checks remain)
**Re-verification:** No — initial verification

---

## Decision Table D-01 through D-20

| # | Decision | Status | Evidence |
|---|----------|--------|----------|
| D-01 | info-icon TouchArea opens `notes-popup` (not summary-popup) | VERIFIED | `card.slint:159` — `info-hover` TouchArea `clicked => { notes-popup.show(); root.on-notes-popover-opened(); }` |
| D-02 | notes-popup uses `close-on-click-outside` + Esc | VERIFIED | `card.slint:842` — `close-policy: close-on-click-outside`; `card.slint:849-856` — `FocusScope` key-pressed handling `Key.Escape` calls `notes-popup.close()` |
| D-03 | Fixed height with internal scroll; empty state "No notes yet"; no lazy-load | VERIFIED | `card.slint:846` — `height: 380px`; `card.slint:972-980` — `if root.notes.length == 0 : Text { text: "No notes yet" }`; notes loaded on popover-open (Plan 05 wiring, no pagination) |
| D-04 | Notes sort: newest-first | VERIFIED | `sqlite.rs` — `read_notes` uses `ORDER BY id DESC` (confirmed in Plan 01 SUMMARY; consistent across all three read paths) |
| D-05 | Note row: author + relative-time shown; hover reveals absolute datetime | VERIFIED | `card.slint:1009-1026` — `note.author-display` + `note.relative-time` rendered; `note.date` Text has `opacity: note-row-hover.has-hover ? 1.0 : 0.0` |
| D-06 | Notes append-only; no edit/delete | VERIFIED | No edit/delete UI or callbacks in `card.slint` notes-popup block; `on-post-note` is write-only |
| D-07 | Composer collapsed to "Add note" button; click expands to multi-line TextInput + Send | VERIFIED | `card.slint:875-965` — `if !root.composer-expanded` shows `"+ Add note"` button; `if root.composer-expanded` shows `composer-input` TextInput + Send Rectangle |
| D-08 | Ctrl+Enter submits, Enter inserts newline | VERIFIED | `card.slint:916-923` — `if (event.modifiers.control && event.text == Key.Return)` triggers submit; no special handling for plain Enter (default inserts newline in `single-line: false` TextInput) |
| D-09 | Optimistic submit — note appears instantly, composer collapses, no pending indicator | VERIFIED | `main.rs:3485` — `on_card_post_note` calls `save_note` (SQLite write) + `apply_filters` for immediate display; `card.slint:918-921` — after submit, `note-draft` cleared and `composer-expanded = false` |
| D-10 | Failure via toast/error pipeline; no inline error in popover | VERIFIED | `live_client.rs:467-481` — `fetch_notes_for_card` uses `eprintln!` on error and always calls `on_complete`; no popover-internal error UI |
| D-11 | SQLite read path; popover-open also triggers targeted GH fetch | VERIFIED | `view_model.rs:206` — `notes: Vec<NoteDisplayEntry>` populated by `enrich_view_with_notes` from SQLite; `main.rs:3411` — `on_card_notes_popover_opened` calls `client.fetch_notes_for_card` which calls `list_issue_comments` then `upsert_notes_for_card` then patches Slint model |
| D-12 | Write path via existing `pending_edit` / `save_note` mechanism | VERIFIED | `main.rs:3485` — `on_card_post_note` calls `save_note` (existing mechanism); `live_client.rs` has `fetch_notes_for_card` posting via `create_issue_comment` on flush |
| D-13 | Offline behavior: note queued in pending_edit, appears in history, flusher retries | VERIFIED | Pattern inherited from Phase 20 offline-first model; `save_note` writes to SQLite immediately; GH post is async via flusher |
| D-14 | `note-preview`, `editing-note`, `save-note`, `summary-clicked` absent from `card.slint` | VERIFIED | Grep for `note-preview\|editing-note\|save-note\|summary-clicked` in `card.slint` returns 0 matches |
| D-15 | Row 4 height 80px; inner product squares 44x44 | VERIFIED | `card.slint:383` — `height: 80px` on Row 4 Rectangle; `card.slint:398-400` — inner clipped Rectangle `width: 44px; height: 44px`; add button `y: 18px; width: 44px; height: 44px` |
| D-16 | `summary-popup` deleted from `card.slint` | VERIFIED | Grep for `summary-popup` across `crates/` returns 0 functional references (only 2 comment-string occurrences in `recipient-detail.slint:2` and `main.rs:6827`) |
| D-17 | Sidebar: avatar + name side-by-side; Purpose, Rx OD/OS editable; Discord editable; Shopify email + link; conditional ww-recipient link | VERIFIED | `recipient-detail.slint:94-130` — avatar (64px circle) + name HorizontalLayout; lines 132-194 Purpose editable; 196-258 Rx OD editable; 260-322 Rx OS editable; 343-407 Discord editable; 409-443 Shopify email + conditional link; 445-463 `if root.recipient-issue-url != ""` conditional ww-recipient block |
| D-18 | Sidebar in Recipients tab only; NOT on main dashboard | VERIFIED | `dashboard.slint:627` — `if root.show-option-grid && root.show-recipient-grid && root.recipient-detail-visible : RecipientDetailPanel` — gated on `show-recipient-grid` which is only true in Recipients tab; no `RecipientDetailPanel` in filtered card view or other tab blocks |
| D-19 | ByRecipient tile-click opens/switches sidebar; card name-click navigates to Recipients tab + auto-selects | VERIFIED | `main.rs:3383-3388` — `on_tile_clicked` branches on `DiscoveryMode::ByRecipient` to call `populate_recipient_sidebar` + `set_recipient_detail_visible(true)`; `main.rs:5981-5999` — `on_card_name_navigate` calls `set_mode_by_index(2)` + `select_tile` + `invoke_tab_clicked(2)` + `populate_recipient_sidebar` + `set_recipient_detail_visible(true)` |
| D-20 | X button + Esc dismiss sidebar; no click-outside (mounted sidebar, not popup) | VERIFIED | `recipient-detail.slint:87-90` — close-touch TouchArea calls `root.close-clicked()`; `recipient-detail.slint:53-65` — FocusScope Esc first cancels active edit, then calls `root.close-clicked()`; `main.rs:5966-5974` — `on_sidebar_close` sets `recipient_detail_visible(false)` + resets editing state; no PopupWindow — mounted component, so no click-outside path |

**Score:** 20/20 decisions verified in code

---

## Supporting Verification

### Artifact Existence and Substance

| Artifact | Status | Evidence |
|----------|--------|----------|
| `crates/core/src/domain/note.rs` | VERIFIED | `NoteEntry { date, content, author: Option<String> }` with `Default` derive |
| `crates/service/src/db/migrations/V012__notes_author.sql` | VERIFIED | `ALTER TABLE notes ADD COLUMN author TEXT`; LF-only line endings confirmed by binary inspection |
| `crates/integrations/src/github/issues_client.rs` | VERIFIED | `GhNoteComment` struct; `list_issue_comments(i64)` method; `parse_note_comment`; `parse_comments_json` pub(crate) |
| `crates/app/src/dashboard/view_model.rs` | VERIFIED | `NoteDisplayEntry` struct; `format_relative_time`; `iso8601_to_epoch_secs`; `notes: Vec<NoteDisplayEntry>` on `DashboardCardViewModel` |
| `crates/app/src/main.rs` | VERIFIED | `enrich_view_with_notes` (2 call sites); `on_card_notes_popover_opened`; `on_card_post_note`; `on_card_name_navigate` (full three-step implementation); `on_tile_clicked` ByRecipient branch; `on_sidebar_close`; `populate_recipient_sidebar` |
| `crates/app/ui/card.slint` | VERIFIED | `NoteDisplayData` struct exported; `notes-popup` PopupWindow with composer + scroll list + hover-reveal; `on-post-note`/`on-notes-popover-opened` callbacks; Row 4 at 80px/44x44; all Row 5 remnants absent |
| `crates/app/ui/recipient-detail.slint` | VERIFIED | 466-line `RecipientDetailPanel`; avatar+name header; 4 inline-edit fields; conditional ww-recipient link; X button + FocusScope Esc |
| `crates/app/ui/dashboard.slint` | VERIFIED | `RecipientDetailPanel` imported; `recipient-detail-visible` property; sidebar mounted in Recipients tab block only |
| `crates/app/ui/option-grid.slint` | VERIFIED | `selected-tile-name: string` property on `RecipientGrid`; tile highlight using `Colors.surface-elevated`/`Colors.accent` |
| `.planning/DATA-FLOW.md` | VERIFIED | `notes.author: Option<String>` documented; V012 migration bullet present |

### Key Links (Wiring)

| From | To | Via | Status |
|------|----|-----|--------|
| `info-hover` TouchArea (card.slint) | `notes-popup.show()` + `on-notes-popover-opened()` | Direct call | VERIFIED |
| `on_card_notes_popover_opened` (main.rs) | `fetch_notes_for_card` (live_client.rs) | `client_rc.fetch_notes_for_card(cid, on_complete)` | VERIFIED |
| `fetch_notes_for_card` | `list_issue_comments` + `upsert_notes_for_card` | background thread | VERIFIED |
| `on_complete` closure | Slint model patch via `invoke_from_event_loop` | `Weak<DashboardWindow>` + `Arc<SqliteStore>` | VERIFIED |
| `DashboardCardViewModel.notes` | `CardData.notes: [NoteDisplayData]` in Slint | `main.rs` CardData initializer maps `vm.notes` | VERIFIED |
| `name-area` TouchArea (card.slint) | `on_card_name_navigate` (main.rs) | `card-name-clicked(recipient-id)` → `card-name-navigate(rid)` forwarding in dashboard.slint | VERIFIED |
| `on_tile_clicked` ByRecipient branch | `populate_recipient_sidebar` + `set_recipient_detail_visible(true)` | Direct call in main.rs | VERIFIED |
| `close-clicked` callback (recipient-detail.slint) | `on_sidebar_close` (main.rs) | `sidebar-close()` forwarded from dashboard.slint | VERIFIED |

### Build Health

| Check | Result |
|-------|--------|
| `cargo check --workspace` | PASS (0 errors; 1 pre-existing unused-variable warning in assignment.rs) |
| V012 migration line endings | LF only (binary-verified) |
| `summary-popup` functional references in crates/ | 0 (2 comment-only occurrences) |
| `note-preview` / `editing-note` / `save-note` in card.slint | 0 matches |
| `note_preview` / `single_line_note_preview` / `apply_optimistic_note_update` in src/ | 0 (1 comment-only in main.rs) |

### Anti-Patterns Scanned

No blockers found. The one pre-existing unused-variable warning in `dashboard/assignment.rs` predates this phase and is outside scope. All new code is substantive — no placeholder returns, no hardcoded empty arrays passed to rendering paths, no TODO/FIXME comments in modified files.

---

## Human Verification Required

### 1. Ctrl+Enter Submit in Composer

**Test:** Open a card's notes popover. Type text in the composer. Press Ctrl+Enter.
**Expected:** Note appears immediately at the top of the notes list; composer collapses back to "+ Add note" button; note-draft cleared.
**Why human:** Slint key modifier event (`event.modifiers.control && event.text == Key.Return`) requires the running Slint event loop; cannot be exercised statically.

### 2. Nested Esc Priority (Composer vs Popover Dismiss)

**Test:** Open the notes popover. Expand the composer. Press Esc.
**Expected:** Composer collapses (draft cleared); popover stays open. Press Esc again — popover closes.
**Why human:** Two FocusScope Esc handlers nested inside a PopupWindow; priority depends on focus ownership at runtime.

### 3. Click-Outside Closes Notes Popover

**Test:** Open the notes popover. Click anywhere outside it.
**Expected:** Popover closes cleanly; no crash; card remains in its current state.
**Why human:** `close-on-click-outside` is a Slint runtime dismissal policy, not testable via static analysis.

### 4. Card Name-Click Cross-Tab Navigation

**Test:** From the main dashboard, click the recipient name on any card.
**Expected:** App switches to the Recipients tab; the correct recipient tile is highlighted; the detail sidebar opens with that recipient's data (name, Rx OD/OS, Discord, Shopify email).
**Why human:** Three-step navigation (set_mode_by_index + invoke_tab_clicked + populate_sidebar) involves Slint event loop ordering that must be verified live.

### 5. Sidebar X-Button Dismiss

**Test:** In the Recipients tab, click a tile to open the sidebar. Click the X button at top right of the sidebar.
**Expected:** Sidebar slides/disappears; tile stays highlighted; no editing state leaks.
**Why human:** Sidebar dismissal and editing-state reset via `on_sidebar_close` must be confirmed in a live session.

### 6. Two-Level Esc in Sidebar (Cancel Edit vs Dismiss)

**Test:** In the Recipients tab sidebar, click the Purpose field to begin editing. Press Esc.
**Expected:** Edit cancelled; field reverts to previous value; sidebar stays open. Press Esc again — sidebar closes.
**Why human:** FocusScope Esc priority (cancel-edit-first then dismiss) depends on which element holds focus at runtime.

### 7. Note Row Hover Timestamp Reveal

**Test:** Open the notes popover on a card that has notes. Hover over a note row.
**Expected:** The absolute ISO date appears (opacity animates to 1.0); moving away hides it again.
**Why human:** Hover-state opacity transition requires live mouse interaction to confirm the `note-row-hover.has-hover` binding fires correctly.

---

## Summary

All 20 decisions (D-01 through D-20) are present and correctly wired in the codebase. The implementation matches the CONTEXT.md spec:

- Notes popover (`notes-popup`) is anchored to the (i) button with `close-on-click-outside` + Esc dismiss, a collapsible Ctrl+Enter composer, scrollable newest-first note list with hover-reveal timestamps, and a "No notes yet" empty state.
- The old Row 5 note preview block is fully removed; product squares are grown to 80px row height with 44x44 inner squares.
- `summary-popup` is completely deleted (0 functional references remain).
- `recipient-detail.slint` implements the full sidebar: avatar + name header, editable Purpose/Rx OD/Rx OS/Discord, conditional Shopify link and ww-recipient link, X + Esc dismissal.
- The sidebar is mounted exclusively in the Recipients tab (`show-recipient-grid` guard) and never appears on the main dashboard.
- ByRecipient tile-click opens the sidebar; card name-click navigates to the Recipients tab and auto-selects.
- The full notes data pipeline is wired: SQLite (V012 migration + `upsert_notes_for_card`) → `NoteDisplayEntry` view model → `NoteDisplayData` Slint struct → `notes-popup` render; popover-open triggers targeted GH fetch via `list_issue_comments`.
- `cargo check --workspace` exits 0.

**Recommendation:** Proceed to UAT using the 7 human verification items above. No code gaps found; the phase is ready for live testing.

---

_Verified: 2026-04-15T12:00:00Z_
_Verifier: Claude (gsd-verifier)_
