# Phase 10: Refresh & Auto-Archive Runtime Wiring - Research

**Researched:** 2026-03-20
**Domain:** Rust/Slint callback wiring — refresh dispatch, archive pipeline, in-flight UI state
**Confidence:** HIGH

---

<user_constraints>
## User Constraints (from CONTEXT.md)

### Locked Decisions

#### Refresh callback wiring
- Refresh-all button `clicked` handler calls `dispatch_refresh_all` (already exists in main.rs)
- Per-card Refresh menu item calls `RefreshDispatcher::dispatch(RefreshCommand::RefreshRecipient { recipient_id })`
- After refresh-all: re-fetch all snapshots via `fetch_card_snapshots`, full card model rebuild, then re-run the complete filter/sort pipeline (search, status, archive filter, discovery sort)
- After per-card refresh: re-fetch just that recipient's snapshot and update its entry in the Slint model in-place (no full rebuild)
- NoopClient refresh methods succeed and re-serve existing seed snapshots (so refresh flow is testable during development)

#### Auto-archive hook timing
- `compute_archive_state` runs on every card load/rebuild — initial load, refresh-all, and per-card refresh
- Auto-archive is silent — no toast notification when cards transition to TBA during refresh
- Cards auto-archived from "Returned" status enter TBA state (dimmed but visible), giving users time to notice before they fully archive
- `promote_all_tba` runs on app startup AND after every full rebuild (refresh-all), so TBA cards that aged past 12h get promoted without needing a restart
- Manual unarchive override continues to prevent re-auto-archive (existing behavior, no change)

#### Post-refresh UI feedback
- Refresh-all button: text changes to "Refreshing..." and button is disabled while in-flight; reverts to "Refresh all" on completion
- Per-card refresh: Refresh menu item shows "Refreshing..." and is disabled while in-flight
- Per-card refresh failure: ellipsis menu button changes to warning color (amber/orange); inside the menu, Refresh item shows "Refresh failed - retry" text; warning color reverts on next successful refresh
- Refresh-all failure: toast notification "Refresh failed" (global action merits a toast, unlike per-card which uses inline indicators)

### Claude's Discretion
- Exact warning color for ellipsis button on per-card refresh failure
- Whether per-card refresh re-runs filters for just that card or skips (since single-card update is minimal)
- Toast duration and positioning for refresh-all failure toast (can reuse existing toast infrastructure)
- How to track per-card refresh-in-flight state (likely CardRefreshState enum already exists)

### Deferred Ideas (OUT OF SCOPE)
None — discussion stayed within phase scope
</user_constraints>

---

<phase_requirements>
## Phase Requirements

| ID | Description | Research Support |
|----|-------------|-----------------|
| DATA-05 | User can trigger refresh for an individual card | Per-card callback wiring in main.rs via RefreshDispatcher; CardRefreshState.Refreshing disables control in-flight |
| DATA-06 | User can trigger refresh-all from UI and via F5 | dispatch_refresh_all + dispatch_key_input already exist; refresh-all TouchArea in dashboard.slint needs clicked handler |
| ARCH-02 | Cards auto-archive when status transitions to Returned | compute_archive_state called in card projection loop; ArchiveStore.compute_and_store called per card during rebuild |
</phase_requirements>

---

## Summary

Phase 10 is a pure wiring phase — all the domain logic is already tested and present. The three targets are: (1) the refresh-all button's TouchArea `clicked` handler, which currently has no callback; (2) the per-card Refresh menu item's `refresh-menu-touch clicked` handler, which closes the menu but emits no event; and (3) the card projection loop in `main()`, which does not yet call `compute_archive_state` or update `archive_state` on `CardData`.

The existing infrastructure is rich. `dispatch_refresh_all` and `dispatch_key_input` are already defined in main.rs. `RefreshDispatcher` and `RefreshCommand` are fully tested in `actions.rs`. `CardRefreshState` (Idle/Refreshing/Error), `CardUiState`, and `DashboardRuntime::mark_refreshing`/`mark_refresh_error` are already wired. `ArchiveStore::compute_and_store` and `promote_all_tba` are fully tested. The only missing pieces are the Slint-to-Rust signal connections and the archive call in the rebuild loop.

The main risk in this phase is correctly scoping the in-flight state for per-card refresh — the `CardData` struct in Slint does not yet surface `refresh_state` from `CardUiState`, and the ellipsis button has no error-color property. This phase must add those surface properties (or add a new `show-error` path via the existing `show-error` bool that already exists) and wire them through the card projection. The `show-error` property already exists on `RecipientCard` but is currently unused for refresh; the planner should decide whether to reuse it or add a separate `refresh-error` property.

**Primary recommendation:** Wire all three gaps in a single plan wave: (1) add Slint callback declarations + UI state properties, (2) wire refresh callbacks in main.rs with correct in-flight state lifecycle, (3) hook `compute_archive_state` into the card projection loop and `promote_all_tba` into the post-rebuild step.

---

## Standard Stack

This phase is pure Rust + Slint — no new dependencies required.

### Core (already in project)
| Component | Location | Purpose |
|-----------|----------|---------|
| `RefreshDispatcher` | `crates/app/src/dashboard/actions.rs` | Executes refresh commands against DashboardDataClient |
| `RefreshCommand` | `crates/app/src/dashboard/actions.rs` | `RefreshAll` and `RefreshRecipient { recipient_id }` variants |
| `RefreshReceipt` | `crates/app/src/service_client.rs` | `succeeded: bool`, `error: Option<String>`, `scope: RefreshScope` |
| `CardRefreshState` | `crates/app/src/dashboard/state.rs` | `Idle`, `Refreshing`, `Error` — already on `CardUiState` |
| `DashboardRuntime::mark_refreshing` | `crates/app/src/dashboard/mod.rs` | Sets refresh state on `card_ui` entry; clears `error_chip` |
| `DashboardRuntime::mark_refresh_error` | `crates/app/src/dashboard/mod.rs` | Sets `CardRefreshState::Error` and `error_chip` message |
| `ArchiveStore::compute_and_store` | `crates/app/src/dashboard/archive.rs` | Computes and persists archive state for one card |
| `ArchiveStore::promote_all_tba` | `crates/app/src/dashboard/archive.rs` | Promotes aged TBA cards to Archived in bulk |
| `dispatch_refresh_all` | `crates/app/src/main.rs` (line 18) | Wraps `DashboardView::run_refresh(RefreshCommand::RefreshAll)` |
| `dispatch_key_input` | `crates/app/src/main.rs` (line 22) | Routes F5 to `dispatch_refresh_all`; already wired but result unused |
| `show_archive_toast` | `crates/app/src/main.rs` (line 861) | `Rc<dyn Fn(String, bool)>` — reuse for refresh-all failure toast |

### No New Dependencies
All required infrastructure is present. This phase adds no new Cargo dependencies.

---

## Architecture Patterns

### Current State of Refresh Infrastructure

The `dispatch_refresh_all` function exists at module level in main.rs but is never called from a UI callback. The `dispatch_key_input` function exists and is tested but its return value is suppressed with `let _ = ...` (line 1487). Neither function is connected to any Slint `clicked` handler.

The per-card Refresh menu item in `card.slint` (line 596-601) currently only calls `card-menu.close()` on click — no callback fires to Rust.

### Current State of Archive Hook

The `archive_store` field exists on `DashboardRuntime`. `promote_all_tba` is called once at startup (lines 610-613). But `compute_archive_state`/`compute_and_store` is never called during card load or rebuild — `archive_state` on `CardData` is only mutated by the explicit manual archive/unarchive callbacks.

### Pattern: Refresh-All Wiring

The refresh-all button (dashboard.slint line 243) has a `TouchArea` with no `clicked` handler. The pattern follows all other callbacks:

```rust
// Source: main.rs callback wiring pattern (existing)
{
    let weak = window.as_weak();
    let rt = runtime.clone();
    let cards = all_cards_ref.clone();
    let client_rc = client.clone();
    let show_toast = show_archive_toast.clone();
    window.on_refresh_all_clicked(move || {
        // 1. Mark in-flight (disable button, change label)
        if let Some(w) = weak.upgrade() {
            w.set_refresh_all_label("Refreshing...".into());
            w.set_refresh_all_disabled(true);
        }
        // 2. Dispatch
        let receipt = dispatch_refresh_all(client_rc.as_ref());
        // 3. On success: rebuild cards, run archive hook, run filter pipeline
        // 4. On failure: show toast
        // 5. Restore button state
    });
}
```

A new Slint callback `refresh-all-clicked` must be declared on `DashboardWindow`, and a new property `refresh-all-disabled: bool` must be added. The existing `refresh-all-label` property already exists (line 46).

### Pattern: Per-Card Refresh Wiring

The `refresh-menu-touch clicked` in card.slint must emit a new `refresh-clicked` callback up through the card grid to main.rs. The pattern for card-level callbacks is already established (see `on_card_archive`, `on_card_unarchive`, etc.) — they take an `int` index, look up the card via `w.get_cards().row_data(idx)`, extract the identifier, and dispatch.

```rust
// Source: main.rs on_card_archive pattern (line 889)
window.on_card_refresh(move |idx| {
    let Some(w) = weak.upgrade() else { return };
    let Some(card_data) = w.get_cards().row_data(idx as usize) else { return };
    let recipient_id = card_data.recipient_name.to_string(); // seed data uses name as ID
    // Mark refreshing on that card
    // Dispatch RefreshCommand::RefreshRecipient { recipient_id }
    // On success: update card in-place, re-run archive for that card
    // On failure: mark error on card
});
```

### Pattern: Surfacing Per-Card Refresh State to Slint

`CardRefreshState` is tracked in `DashboardRuntime::card_ui` (a `HashMap<String, CardUiState>`). For the refresh-disabled and error-color states to appear in Slint, they must flow through the card projection. Currently `CardData` has `refresh_disabled: bool` and `refresh_label: string` fields in the Slint model (visible in seed_cards, lines 146-147). These must be updated during card push, sourced from `runtime.card_ui.get(&recipient_id)`.

For the ellipsis button warning color (per-card refresh error), a new `show-refresh-error: bool` property on `RecipientCard` is the cleanest approach. The existing `show-error` bool drives the stale/retry button (line 637) and is not suitable for reuse without semantic confusion.

### Pattern: Auto-Archive Hook in Card Rebuild

`compute_and_store` must be called for every card in the projection loop. The call sequence:

```rust
// Source: archive.rs ArchiveStore::compute_and_store signature
// Called once per card during projection, passing status_pill and SystemTime::now()
let archive_rec = runtime.archive_store.compute_and_store(
    &recipient_id,
    &card.status_pill,
    SystemTime::now(),
);
let archive_state_int: i32 = match archive_rec.state {
    ArchiveState::Active => 0,
    ArchiveState::ToBeArchived => 1,
    ArchiveState::Archived => 2,
};
card.archive_state = archive_state_int;
```

`promote_all_tba` must be called after every full rebuild (refresh-all), mirroring the startup call:

```rust
// Source: main.rs lines 610-613 (startup pattern)
rt.archive_store.promote_all_tba(SystemTime::now());
```

### Anti-Patterns to Avoid

- **Full rebuild on per-card refresh:** The decision is in-place update only. Do not call `apply_filters` with the full card set after a single-card refresh — only update the one `CardData` entry in `all_cards_ref` and call `apply_filters` once.
- **Calling compute_archive_state directly instead of compute_and_store:** `compute_and_store` both computes and persists in `ArchiveStore`. Using the free function `compute_archive_state` directly bypasses persistence.
- **Borrowing runtime mutably in closures that also borrow all_cards_ref:** The existing pattern uses separate `borrow_mut()` scopes to avoid double-borrow panics. Keep archive store mutation inside its own borrow scope before calling `apply_filters`.
- **Sharing the refresh-all button's `disabled` state as a Slint global property vs. per-component:** Use a window-level bool property, not a per-card property, for refresh-all in-flight state.

---

## Don't Hand-Roll

| Problem | Don't Build | Use Instead | Why |
|---------|-------------|-------------|-----|
| Refresh dispatch | Custom client call | `dispatch_refresh_all` / `RefreshDispatcher::dispatch` | Already wraps error handling and scope |
| Per-card in-flight tracking | New bool field | `DashboardRuntime::mark_refreshing` / `mark_refresh_error` | Already handles state transitions and error_chip clearing |
| TBA promotion | Custom age check | `ArchiveStore::promote_all_tba` | Already handles batch promotion; `promote_tba_if_due` is the per-record version |
| Auto-archive computation | Inline status comparison | `ArchiveStore::compute_and_store` | Encapsulates one-way latch, manual override, TBA timestamp |
| Toast for refresh-all failure | New toast mechanism | `show_archive_toast` Rc closure | Already declared; reuse with `has_undo: false` |

---

## Common Pitfalls

### Pitfall 1: Missing Callback Declaration in Slint

**What goes wrong:** Adding an `on_refresh_all_clicked` handler in main.rs compiles fine against the Slint generated code, but the TouchArea `clicked` handler in dashboard.slint never calls the callback — so the button remains silent.

**Why it happens:** Slint requires explicit `callback` declarations on the component AND explicit `root.callback-name()` calls in the event handler.

**How to avoid:** For every new wiring: (1) declare `callback refresh-all-clicked;` on `DashboardWindow` in dashboard.slint, (2) add `root.refresh-all-clicked();` inside the TouchArea `clicked =>` body, (3) wire `on_refresh_all_clicked` in main.rs.

**Warning signs:** Handler in main.rs registers without error but behavior never fires at runtime.

### Pitfall 2: Double-Borrow Panic on Runtime + Cards

**What goes wrong:** Calling `rt.borrow_mut()` inside a closure that also has an active `all_cards_ref.borrow()` causes a runtime panic if both borrows overlap.

**Why it happens:** `Rc<RefCell<>>` panics on concurrent borrows. Closures often hold borrows longer than expected.

**How to avoid:** Scope borrows explicitly using block `{ }` delimiters. Look at the `on_card_archive` pattern (lines 895-908) — `rt.borrow_mut()` and `cards.borrow_mut()` are separate consecutive scopes, never nested.

### Pitfall 3: Archive State Not Flowing Into Slint Model

**What goes wrong:** `compute_and_store` updates `ArchiveStore` in `DashboardRuntime`, but `CardData.archive_state` on the `all_cards_ref` Vec is not updated. Slint renders the old value.

**Why it happens:** `ArchiveStore` and `all_cards_ref` are separate data structures. The archive state integer on `CardData` must be explicitly synced after every `compute_and_store` call — the same way manual archive callbacks do it (lines 900-903).

**How to avoid:** After calling `compute_and_store` in the rebuild loop, immediately update `card.archive_state = archive_state_int` on the `CardData` entry in `all_cards_ref`.

### Pitfall 4: Seed Data Recipient ID Gap

**What goes wrong:** `RefreshCommand::RefreshRecipient { recipient_id }` expects a real `recipient_id`, but seed `CardData` structs have no stable ID — they use `recipient_name` as a pseudo-ID in callbacks (see `on_card_archive` pattern, line 896).

**Why it happens:** Seed data was built before the full projection pipeline existed. `all_cards_ref` uses name-based lookup.

**How to avoid:** For the seed data phase, extract `recipient_name` as the `recipient_id` (matching the archive callback pattern). When production projection is wired, this will be replaced with `snapshot.recipient_id`. Document the temporary ID mapping in a comment.

### Pitfall 5: Per-Card Refresh Fails Silently If Error State Not Surfaced

**What goes wrong:** `DashboardRuntime::mark_refresh_error` sets `error_chip` on `card_ui`, but if that state is never read during the card projection step that updates `CardData`, the ellipsis button will not change color.

**Why it happens:** `card_ui` HashMap is not automatically reflected to Slint. The projection step must explicitly read `refresh_state` from `card_ui` and set `refresh_disabled` and any new error properties on `CardData`.

**How to avoid:** In the card projection step (where `CardData` fields are set for display), always read `runtime.card_ui.get(&id)` and apply `refresh_state` and `error_chip` to the relevant `CardData` fields.

### Pitfall 6: Slint Popup Menu Does Not Close After Callback

**What goes wrong:** The Refresh menu item closes the popup via `card-menu.close()` but if the in-flight state makes the item disabled and re-opened, the previous close is lost.

**Why it happens:** `card-menu.close()` is already in the `refresh-menu-touch clicked =>` handler. This is the correct pattern — but the disable logic must also prevent the TouchArea from being clickable when `refresh-disabled` is true.

**How to avoid:** The existing `mouse-cursor: root.refresh-disabled ? default : pointer` pattern (card.slint line 597) already signals non-interactivity. Add a guard in the callback: `if !root.refresh-disabled { root.refresh-clicked(); card-menu.close(); }`.

---

## Code Examples

### Refresh-All Callback Wiring (main.rs pattern)
```rust
// Source: existing on_card_archive pattern — adapted for refresh-all
{
    let weak = window.as_weak();
    let rt = runtime.clone();
    let cards = all_cards_ref.clone();
    let client_rc = client.clone();
    let show_toast = show_archive_toast.clone();
    window.on_refresh_all_clicked(move || {
        let Some(w) = weak.upgrade() else { return };
        w.set_refresh_all_label("Refreshing...".into());
        w.set_refresh_all_disabled(true);
        let receipt = dispatch_refresh_all(client_rc.as_ref());
        if receipt.succeeded {
            // Re-run archive hook on all cards, then promote_all_tba
            let now = SystemTime::now();
            {
                let mut cards_mut = cards.borrow_mut();
                let mut rt_mut = rt.borrow_mut();
                for card in cards_mut.iter_mut() {
                    let id = card.recipient_name.to_string();
                    let rec = rt_mut.archive_store.compute_and_store(&id, &card.status_pill.to_string(), now);
                    card.archive_state = match rec.state {
                        ArchiveState::Active => 0,
                        ArchiveState::ToBeArchived => 1,
                        ArchiveState::Archived => 2,
                    };
                }
                rt_mut.archive_store.promote_all_tba(now);
            }
            apply_filters(&w, &cards.borrow(), &rt.borrow());
        } else {
            let msg = receipt.error.unwrap_or_else(|| "Refresh failed".to_string());
            show_toast(msg, false);
        }
        w.set_refresh_all_label("Refresh all".into());
        w.set_refresh_all_disabled(false);
    });
}
```

### Auto-Archive in Card Projection Loop
```rust
// Source: archive.rs ArchiveStore::compute_and_store
// Called once per card when building the initial all_cards_ref Vec
// (also called again in the refresh-all rebuild path above)
let now = SystemTime::now();
for card in all_cards.iter_mut() {
    let id = card.recipient_name.to_string(); // seed data uses name as ID
    let rec = runtime.archive_store.compute_and_store(
        &id,
        &card.status_pill.to_string(),
        now,
    );
    card.archive_state = match rec.state {
        ArchiveState::Active => 0,
        ArchiveState::ToBeArchived => 1,
        ArchiveState::Archived => 2,
    };
}
```

### Slint Callback Declaration (dashboard.slint)
```slint
// Add to DashboardWindow declarations
callback refresh-all-clicked;
in property <bool> refresh-all-disabled: false;

// In the TouchArea inside the refresh-all Rectangle (line 243):
TouchArea {
    mouse-cursor: root.refresh-all-disabled ? default : pointer;
    clicked => {
        if !root.refresh-all-disabled {
            root.refresh-all-clicked();
        }
    }
}
// Text label already uses root.refresh-all-label — no change needed there
```

### Slint Per-Card Refresh Callback (card.slint)
```slint
// Add callback declaration to RecipientCard component
callback refresh-clicked();

// New property for error color on ellipsis button
in property <bool> refresh-error: false;

// Ellipsis button background (already at line 435) — add error tint:
background: refresh-error ? #5a3010 : (dots-touch.has-hover ? #3a4060 : transparent);

// In refresh-menu-touch clicked => (line 598-600):
refresh-menu-touch := TouchArea {
    mouse-cursor: root.refresh-disabled ? default : pointer;
    clicked => {
        if !root.refresh-disabled {
            root.refresh-clicked();
        }
        card-menu.close();
    }
}
```

### Per-Card Refresh Callback (main.rs)
```rust
// Wire on_card_refresh in main.rs following on_card_archive pattern
{
    let weak = window.as_weak();
    let rt = runtime.clone();
    let cards = all_cards_ref.clone();
    let client_rc = client.clone();
    window.on_card_refresh(move |idx| {
        let Some(w) = weak.upgrade() else { return };
        let Some(card_data) = w.get_cards().row_data(idx as usize) else { return };
        let recipient_id = card_data.recipient_name.to_string();
        // Mark in-flight
        {
            let mut rt_mut = rt.borrow_mut();
            rt_mut.mark_refreshing(&recipient_id, true);
        }
        // Update card display (refresh_disabled=true, refresh_label="Refreshing...")
        // ... update cards_ref for this card ...
        apply_filters(&w, &cards.borrow(), &rt.borrow());

        // Dispatch
        let receipt = RefreshDispatcher::new(client_rc.as_ref())
            .dispatch(RefreshCommand::RefreshRecipient { recipient_id: recipient_id.clone() });

        // Update state based on result
        {
            let mut rt_mut = rt.borrow_mut();
            if receipt.succeeded {
                rt_mut.mark_refreshing(&recipient_id, false);
                // Re-run archive state for this card
                let now = SystemTime::now();
                let mut cards_mut = cards.borrow_mut();
                for card in cards_mut.iter_mut() {
                    if card.recipient_name.to_string() == recipient_id {
                        let rec = rt_mut.archive_store.compute_and_store(
                            &recipient_id, &card.status_pill.to_string(), now);
                        card.archive_state = match rec.state {
                            ArchiveState::Active => 0,
                            ArchiveState::ToBeArchived => 1,
                            ArchiveState::Archived => 2,
                        };
                    }
                }
            } else {
                rt_mut.mark_refresh_error(&recipient_id,
                    receipt.error.unwrap_or_else(|| "Refresh failed".to_string()));
            }
        }
        apply_filters(&w, &cards.borrow(), &rt.borrow());
    });
}
```

---

## Integration Points Inventory

### dashboard.slint Changes Required
| Change | Location | What |
|--------|----------|------|
| New callback declaration | DashboardWindow | `callback refresh-all-clicked;` |
| New property | DashboardWindow | `in property <bool> refresh-all-disabled: false;` |
| TouchArea wiring | Refresh-all button (line 243) | Add `clicked => { root.refresh-all-clicked(); }` with disabled guard |

### card.slint Changes Required
| Change | Location | What |
|--------|----------|------|
| New callback declaration | RecipientCard | `callback refresh-clicked();` |
| New property | RecipientCard | `in property <bool> refresh-error: false;` |
| Ellipsis button | Line 435 | Conditional background to show warning tint on `refresh-error` |
| Refresh menu item | Line 596-601 | Add `root.refresh-clicked();` call, guard with `if !root.refresh-disabled` |

### dashboard.slint Card Grid Binding Changes
| Change | Location | What |
|--------|----------|------|
| Wire `refresh-clicked` | Card instance in grid | `refresh-clicked => { root.on_card_refresh(index); }` |
| Wire `refresh-error` | Card instance in grid | `refresh-error: card-data.refresh-error;` (new CardData field) |

### CardData Struct (Slint) Changes Required
| New Field | Type | Purpose |
|-----------|------|---------|
| `refresh-error` | `bool` | Drive ellipsis button warning color |

(The `refresh-disabled` and `refresh-label` fields already exist on `CardData` — just not sourced from runtime yet.)

### main.rs Changes Required
| Change | What |
|--------|------|
| New callback wire: `on_refresh_all_clicked` | Connect to `dispatch_refresh_all` + rebuild + archive hook |
| New callback wire: `on_card_refresh` | Connect to `RefreshDispatcher::dispatch(RefreshRecipient)` + per-card archive update |
| F5 callback: restore `dispatch_key_input` result | Currently suppressed; wire to same rebuild path as `on_refresh_all_clicked` |
| Initial card load | Add `compute_and_store` loop after `seed_cards()` before `apply_filters` |
| `apply_filters` card projection | Read `card_ui` refresh state, set `refresh_disabled`, `refresh_label`, `refresh_error` on `CardData` |

---

## State of the Art

| Old Approach | Current Approach | When Changed | Impact |
|--------------|------------------|--------------|--------|
| Dead refresh button | Wired refresh-all + per-card | Phase 10 | DATA-05, DATA-06 closed |
| No auto-archive on data load | compute_archive_state in projection loop | Phase 10 | ARCH-02 closed |
| promote_all_tba startup-only | Also called after every refresh-all | Phase 10 | TBA cards age correctly without restart |

---

## Open Questions

1. **Does per-card refresh re-run `apply_filters` once or twice?**
   - What we know: The decision context says single-card update is "minimal." The pattern in all other callbacks is to call `apply_filters` once at the end.
   - What's unclear: Whether calling `apply_filters` once after marking in-flight (to show disabled state immediately) and once after completion (to show result) is necessary, or if a single call after completion is sufficient.
   - Recommendation: Call once at the end. The in-flight UI state update (refresh_disabled, refresh_label) can be applied directly to the Slint model via `w.get_cards().set_row_data(idx, updated_card)` rather than via `apply_filters` to avoid flickering.

2. **Exact warning color value for ellipsis button**
   - What we know: Amber/orange family, left to Claude's discretion per CONTEXT.md.
   - Recommendation: Use `#7a3010` for the background tint and `#f0a030` for text (matches existing amber used in archive menu item color at card.slint line 614). Consistent palette, already rendered at runtime.

3. **F5 wiring path**
   - What we know: `dispatch_key_input` exists and routes F5 to `dispatch_refresh_all`. Its result is currently suppressed with `let _`.
   - What's unclear: Where in the Slint FocusScope F5 is captured — whether it goes through the existing key-pressed callback.
   - Recommendation: Check whether `on_key_pressed` or a global key handler in dashboard.slint calls into Rust for key events. If not, F5 needs a Slint key handler wired to `refresh-all-clicked` callback — same as button click. The existing `dispatch_key_input` can be removed in favor of routing through the same callback.

---

## Validation Architecture

### Test Framework
| Property | Value |
|----------|-------|
| Framework | Rust built-in test (`cargo test`) |
| Config file | none — workspace-level Cargo.toml |
| Quick run command | `cargo test -p app --lib 2>&1` |
| Full suite command | `cargo test --workspace 2>&1` |

### Phase Requirements -> Test Map
| Req ID | Behavior | Test Type | Automated Command | File Exists? |
|--------|----------|-----------|-------------------|-------------|
| DATA-05 | Per-card refresh dispatches RefreshRecipient command | unit | `cargo test -p app --lib dashboard::actions 2>&1` | Yes (actions.rs tests) |
| DATA-05 | mark_refreshing sets CardRefreshState::Refreshing | unit | `cargo test -p app --lib dashboard 2>&1` | Yes (mod.rs runtime_tests) |
| DATA-06 | dispatch_refresh_all calls refresh_all on client | unit | `cargo test -p app --lib dashboard::actions 2>&1` | Yes (actions.rs tests) |
| ARCH-02 | compute_and_store returns ToBeArchived for Returned status | unit | `cargo test -p app --lib dashboard::archive 2>&1` | Yes (archive.rs tests) |
| ARCH-02 | promote_all_tba promotes aged TBA after refresh-all | unit | `cargo test -p app --lib dashboard::archive 2>&1` | Yes (archive.rs tests) |

### Sampling Rate
- **Per task commit:** `cargo test -p app --lib 2>&1`
- **Per wave merge:** `cargo test --workspace 2>&1`
- **Phase gate:** Full suite green before `/gsd:verify-work`

### Wave 0 Gaps
None — existing test infrastructure covers the domain logic. New integration tests for the callback wiring are not needed for this phase since the wiring is synchronous and testable by observation (the NoopClient refresh methods succeed, producing observable state changes).

---

## Sources

### Primary (HIGH confidence)
- Direct code inspection: `crates/app/src/dashboard/actions.rs` — RefreshDispatcher, RefreshCommand, tests
- Direct code inspection: `crates/app/src/dashboard/archive.rs` — ArchiveStore, compute_archive_state, promote_all_tba, tests
- Direct code inspection: `crates/app/src/dashboard/mod.rs` — DashboardRuntime, mark_refreshing, mark_refresh_error
- Direct code inspection: `crates/app/src/dashboard/state.rs` — CardRefreshState, CardUiState
- Direct code inspection: `crates/app/src/main.rs` — dispatch_refresh_all, dispatch_key_input, all existing callback patterns, seed data, apply_filters
- Direct code inspection: `crates/app/ui/dashboard.slint` — refresh-all button, callback surface
- Direct code inspection: `crates/app/ui/card.slint` — RecipientCard properties, refresh menu item, ellipsis button
- `.planning/phases/10-refresh-auto-archive-wiring/10-CONTEXT.md` — all locked decisions

### Secondary (MEDIUM confidence)
- `.planning/STATE.md` accumulated context — cross-phase patterns and prior decisions

---

## Metadata

**Confidence breakdown:**
- Standard stack: HIGH — all libraries present, verified in source
- Architecture patterns: HIGH — all integration points identified from direct source inspection
- Pitfalls: HIGH — derived from existing callback patterns and observed gaps in source
- Slint property additions: HIGH — established precedent in card.slint/dashboard.slint patterns

**Research date:** 2026-03-20
**Valid until:** 2026-04-20 (stable codebase, no external dependencies changing)
