---
phase: 10-refresh-auto-archive-wiring
verified: 2026-03-20T23:55:00Z
updated: 2026-03-21T00:30:00Z
status: passed
score: 19/19 must-haves verified
re_verification:
  previous_status: passed
  previous_score: 13/13
  gaps_closed:
    - "Manually-archived cards survive compute_and_store calls during refresh"
    - "Archive state persists across app restarts"
    - "ArchiveStore loads saved state before init loop runs"
    - "ArchiveStore loads persisted state from disk on app startup before init loop"
    - "NoopClient refresh methods return Err so error signaling is testable during development"
    - "Refresh-all and per-card refresh do not wipe manually-archived card state"
  gaps_remaining: []
  regressions: []
---

# Phase 10: Refresh and Auto-Archive Wiring Verification Report

**Phase Goal:** Wire refresh-all and per-card refresh callbacks with auto-archive hooks, archive persistence, and manual-archive preservation.
**Verified:** 2026-03-20T23:55:00Z
**Updated (re-verification):** 2026-03-21T00:30:00Z
**Status:** PASSED
**Re-verification:** Yes — after gap closure plans 10-03 and 10-04

## Goal Achievement

### Observable Truths — Original 13 (Plans 10-01 / 10-02)

| # | Truth | Status | Evidence |
|---|-------|--------|----------|
| 1 | DashboardWindow declares refresh-all-clicked callback and refresh-all-disabled property | VERIFIED | `callback refresh-all-clicked` at line 96, `in property <bool> refresh-all-disabled: false` at line 48 of dashboard.slint |
| 2 | RecipientCard declares refresh-clicked callback and refresh-error property | VERIFIED | `callback refresh-clicked()` at line 68, `in property <bool> refresh-error: false` at line 25 of card.slint |
| 3 | CardData struct has refresh-error bool field | VERIFIED | `refresh-error: bool,` at line 25 of dashboard.slint inside CardData struct |
| 4 | Card grid wires refresh-clicked through to root callback | VERIFIED | `refresh-clicked => { root.card-refresh(card-index)` at lines 423-424 of dashboard.slint |
| 5 | Auto-archive compute_and_store runs on every card during initial seed load | VERIFIED | Block at lines 616-633 of main.rs calls `rt.archive_store.compute_and_store` for each card before `apply_filters` |
| 6 | promote_all_tba runs after initial archive computation | VERIFIED | `rt.archive_store.promote_all_tba(SystemTime::now())` after initial apply_filters in main.rs |
| 7 | Clicking refresh-all triggers dispatch_refresh_all, rebuilds cards, runs archive hook, restores button state | VERIFIED | `window.on_refresh_all_clicked` in main.rs: in-flight disable, dispatch, compute_and_store loop, promote_all_tba, apply_filters, restore |
| 8 | Per-card Refresh menu item dispatches RefreshRecipient, updates that card in-place, runs archive for that card | VERIFIED | `window.on_card_refresh` in main.rs: RefreshDispatcher::dispatch(RefreshRecipient), compute_and_store on success |
| 9 | F5 key triggers the same refresh-all flow as the button | VERIFIED | `if event.text == Key.F5 { root.refresh-all-clicked()` at lines 195-197 of dashboard.slint |
| 10 | Refresh-all failure shows toast notification | VERIFIED | `show_toast(msg, false)` on failure path in main.rs |
| 11 | Per-card refresh failure sets refresh-error on CardData so ellipsis button shows warning color | VERIFIED | `card.refresh_error = true` in main.rs; amber color driven by `root.refresh-error ? (#4a2810/#5a3818)` in card.slint |
| 12 | Refresh-all success re-runs promote_all_tba after rebuild | VERIFIED | `rt_mut.archive_store.promote_all_tba(now)` inside on_refresh_all_clicked success branch |
| 13 | Per-card refresh in-flight disables the Refresh menu item and shows Refreshing label | VERIFIED | `card.refresh_disabled = true`, `card.refresh_label = "Refreshing...".into()`, `rt_mut.mark_refreshing` in main.rs, first `apply_filters` |

### Observable Truths — Gap Closure Plan 10-03 (archive.rs)

| # | Truth | Status | Evidence |
|---|-------|--------|----------|
| 14 | Manually-archived cards survive compute_and_store calls during refresh | VERIFIED | `if rec.state != ArchiveState::Active { return rec.clone(); }` gate at archive.rs lines 105-107; tests `manually_archived_card_survives_compute_and_store` and `manually_archived_then_promoted_survives_recompute` present at lines 544-575 |
| 15 | Archive state persists across app restarts | VERIFIED | `ArchiveStore::load_or_create` reads JSON from disk (archive.rs line 173-181); `persist()` called at end of every mutation method: compute_and_store (line 218), manual_archive (line 231), manual_unarchive (line 243), archive_now (line 253), undo_archive (line 265), promote_all_tba (line 278) |
| 16 | ArchiveStore loads saved state before init loop runs | VERIFIED | `load_or_create` called at main.rs line 612; init loop begins at line 616 — load precedes loop |

### Observable Truths — Gap Closure Plan 10-04 (main.rs / mod.rs)

| # | Truth | Status | Evidence |
|---|-------|--------|----------|
| 17 | ArchiveStore loads persisted state from disk on app startup before init loop | VERIFIED | main.rs lines 608-613: `archive_path = dirs::data_local_dir().join("wit-what").join("archive_store.json")`; `ArchiveStore::load_or_create(archive_path)` at line 612; runtime constructed at line 613; init loop at lines 616-633 — ordering correct |
| 18 | NoopClient refresh methods return Err so error signaling is testable during development | VERIFIED | main.rs lines 1710-1716: `fn refresh_recipient` returns `Err("offline: noop client")` and `fn refresh_all` returns `Err("offline: noop client")` as explicit overrides in `impl DashboardDataClient for NoopClient` |
| 19 | Refresh-all and per-card refresh do not wipe manually-archived card state | VERIFIED | Composite of truth 14 (compute_archive_state preserves manual state) + truth 7/8 (refresh flows call compute_and_store which now uses the fixed function) — the preservation gate and the wiring are both in place |

**Score:** 19/19 truths verified

### Required Artifacts

| Artifact | Expected | Status | Details |
|----------|----------|--------|---------|
| `crates/app/ui/dashboard.slint` | Refresh-all callback, disabled property, card-refresh callback, F5 key handler | VERIFIED | All patterns present (unchanged from original verification) |
| `crates/app/ui/card.slint` | Per-card refresh callback, refresh-error property, ellipsis warning color | VERIFIED | Unchanged from original verification |
| `crates/app/src/dashboard/archive.rs` | compute_archive_state preserves manual archive state; ArchiveStore with serde persistence | VERIFIED | `system_time_serde` module (lines 11-35); `serde::Serialize, serde::Deserialize` on ArchiveState (line 38) and ArchiveRecord (line 50); `#[serde(with = "system_time_serde")]` on archived_at (line 55); `load_or_create` (lines 172-181); `persist()` (lines 184-193); manual-state preservation gate (lines 101-107); 6 new tests (lines 544-643) |
| `crates/app/src/dashboard/mod.rs` | DashboardRuntime constructor accepts archive_store parameter | VERIFIED | `with_archive_store(archive_store: ArchiveStore) -> Self` at lines 76-83; `Default` impl retained using `ArchiveStore::new()` so all existing tests continue to compile (lines 37-45) |
| `crates/app/src/main.rs` | ArchiveStore initialized via load_or_create with wit-what data dir path; NoopClient with Err-returning refresh overrides | VERIFIED | archive_path at lines 608-611; load_or_create at line 612; with_archive_store at line 613; NoopClient refresh_recipient at lines 1710-1712; refresh_all at lines 1714-1716 |

### Key Link Verification

| From | To | Via | Status | Details |
|------|----|-----|--------|---------|
| `card.slint` | `dashboard.slint` | refresh-clicked callback propagation through card grid | WIRED | Unchanged from original verification |
| `dashboard.slint` | `main.rs` | callback refresh-all-clicked consumed by on_refresh_all_clicked | WIRED | Unchanged from original verification |
| `main.rs on_refresh_all_clicked` | `dispatch_refresh_all` | direct function call | WIRED | Unchanged from original verification |
| `main.rs on_card_refresh` | `RefreshDispatcher::dispatch(RefreshRecipient)` | RefreshDispatcher dispatch call | WIRED | Unchanged from original verification |
| `main.rs on_refresh_all_clicked` | `archive_store.compute_and_store` | archive hook in post-refresh rebuild | WIRED | Unchanged from original verification |
| `main.rs on_refresh_all_clicked` | `archive_store.promote_all_tba` | TBA promotion after full rebuild | WIRED | Unchanged from original verification |
| `ArchiveStore::load_or_create` | `archive_store.json on disk` | `serde_json::from_slice` deserialize | WIRED | archive.rs line 175: `serde_json::from_slice(&data).ok()` inside `load_or_create` |
| `ArchiveStore mutation methods` | `archive_store.json on disk` | `self.persist()` call after every mutation | WIRED | `self.persist()` present at lines 218, 231, 243, 253, 265, 278 of archive.rs — all 6 mutation methods covered |
| `main.rs init block` | `ArchiveStore::load_or_create` | `dirs::data_local_dir().join("wit-what").join("archive_store.json")` | WIRED | main.rs lines 608-612: path constructed with exact pattern, then passed to `load_or_create` |
| `NoopClient` | `refresh_recipient / refresh_all` | Err override | WIRED | main.rs lines 1710-1716: both methods explicitly return `Err("offline: noop client")` |

### Requirements Coverage

| Requirement | Source Plan | Description | Status | Evidence |
|-------------|------------|-------------|--------|----------|
| DATA-05 | 10-01, 10-02 | User can trigger refresh for an individual card | SATISFIED | `on_card_refresh` handler fully wired in main.rs |
| DATA-06 | 10-01, 10-02 | User can trigger refresh-all from UI and via F5 | SATISFIED | Refresh-all button and F5 both fire full handler |
| ARCH-02 | 10-01, 10-02, 10-03 | Cards auto-archive when status transitions to Returned; manual archive state preserved through refresh | SATISFIED | compute_archive_state fix at archive.rs lines 101-107 preserves manual TBA/Archived state; serde persistence survives restarts; load_or_create called before init loop |

No orphaned requirements — REQUIREMENTS.md maps exactly DATA-05, DATA-06, ARCH-02 to Phase 10, all claimed and verified.

### Anti-Patterns Found

None. No TODO, FIXME, placeholder comments, or stub implementations found in any modified file.

Note: `dispatch_key_input` has a `dead_code` warning (confirmed in prior build output). This is a test-only function inside `#[cfg(test)]`. The warning is benign and predates this phase.

### Human Verification Required

The following behaviors cannot be verified programmatically:

#### 1. Refresh-all button visual disabled state

**Test:** Run the app, click the Refresh-all button while it is mid-refresh, or inspect the button background color change.
**Expected:** Button background changes from `#4a7cff` to `#3a5299` while refreshing, cursor changes from pointer to default.
**Why human:** Color rendering and cursor behavior require visual inspection.

#### 2. Ellipsis button amber color on per-card refresh failure

**Test:** Run the app with seed data (NoopClient now returns Err for refresh). Click the three-dots menu on any card, select Refresh.
**Expected:** Button background switches to `#4a2810` / `#5a3818` amber tones. Menu shows "Refresh failed - retry".
**Why human:** Requires visual confirmation that the amber state renders correctly end-to-end; NoopClient now returns Err so this path is reachable in development.

#### 3. Archive state persistence across restart

**Test:** Run the app, manually archive a card, quit, relaunch. Observe the card's archive state.
**Expected:** Card is still in TBA/Archived state after restart — state loaded from `{data_local_dir}/wit-what/archive_store.json`.
**Why human:** Requires launching, quitting, and relaunching the app. Programmatic verification confirmed the code path is correct but cannot simulate the full process lifecycle.

#### 4. F5 key global focus behavior

**Test:** Run the app, press F5 from various UI states (search focused, card focused, no focus).
**Expected:** F5 triggers refresh-all in all states where the global FocusScope captures input.
**Why human:** Key event routing through Slint's FocusScope hierarchy requires interactive testing.

### Gaps Summary

No gaps. All 19 automated checks passed, including the 6 new truths from gap closure plans 10-03 and 10-04. The two root issues from the UAT — manual archive state being wiped on refresh, and archive state lost on restart — are both resolved in code and verified.

---

_Initially verified: 2026-03-20T23:55:00Z_
_Re-verified: 2026-03-21T00:30:00Z_
_Verifier: Claude (gsd-verifier)_
