# Phase 16.2: BUGSWEEPER — Technical Research

## RESEARCH COMPLETE

---

## 1. HTTP Server Crate Selection

**Recommendation: `tiny_http`**

- Zero async runtime — just blocking TCP. Perfect for a debug server on a background thread.
- ~200KB compile overhead, minimal dependencies.
- API: `Server::http("127.0.0.1:9876")` → loop `server.recv()` → `request.respond(Response::from_string(json))`.
- Alternatives considered:
  - `warp`/`axum` — require tokio async runtime, massive dependency tree, overkill for debug server
  - `rouille` — thin wrapper over `tiny_http`, adds routing sugar but another dependency
  - Raw `std::net::TcpListener` — possible but manual HTTP parsing is fragile

**Decision:** Use `tiny_http` directly. Route matching can be done with simple string matching on `request.url()` — no framework needed for ~15 endpoints.

## 2. Cross-Thread Communication Pattern

**Critical finding:** `slint::invoke_from_event_loop()` is fire-and-forget.

- Signature: `fn invoke_from_event_loop(f: impl FnOnce() + Send + 'static) -> Result<(), EventLoopError>`
- The closure runs on the Slint UI thread but does NOT return a value to the caller.
- The codebase uses this pattern 11+ times (live_client.rs, main.rs callback handlers).

**Synchronous request-response pattern for BUGSWEEPER:**

```rust
use std::sync::mpsc;
use std::time::Duration;

fn query_ui<T: Send + 'static>(
    weak: &slint::Weak<DashboardWindow>,
    f: impl FnOnce(&DashboardWindow) -> T + Send + 'static,
) -> Result<T, String> {
    let (tx, rx) = mpsc::channel();
    let weak = weak.clone();
    slint::invoke_from_event_loop(move || {
        if let Some(w) = weak.upgrade() {
            let result = f(&w);
            let _ = tx.send(Ok(result));
        } else {
            let _ = tx.send(Err("Window destroyed".to_string()));
        }
    }).map_err(|e| format!("Event loop error: {:?}", e))?;
    rx.recv_timeout(Duration::from_secs(5))
        .map_err(|_| "UI query timed out".to_string())?
}
```

This is the standard pattern for GUI debug tools (Qt Creator, GTK Inspector). The HTTP thread blocks on the channel receiver while the UI thread executes the query and sends the result back.

**For mutations (property writes, callback invocation):** Same pattern, but the closure returns `()` or a status string.

## 3. Slint Type Serialization

### Generated Slint types (CardData, ItemSquareData)

Slint-generated Rust types do NOT derive `serde::Serialize`. They use:
- `slint::SharedString` (not `String`)
- `slint::ModelRc<T>` (not `Vec<T>`)
- `slint::Color` (not a hex string)
- `slint::Image` (opaque handle — not serializable)

**Approach:** Create intermediate JSON-friendly structs in the `bugsweeper` crate:

```rust
#[derive(Serialize)]
struct CardDataJson {
    recipient_name: String,
    status_pill: String,
    status_date: String,
    // ... all fields, with SharedString → String, ModelRc → Vec, Color → hex string
    // Image fields omitted (not meaningful as JSON)
}

fn card_data_to_json(card: &CardData) -> CardDataJson {
    CardDataJson {
        recipient_name: card.recipient_name.to_string(),
        status_pill: card.status_pill.to_string(),
        // ... etc
    }
}
```

### DashboardCardViewModel (Rust-native)

Already has `#[derive(Debug, Clone, PartialEq, Eq)]`. Adding `#[derive(serde::Serialize)]` is trivial EXCEPT:
- `SystemTime` doesn't impl Serialize — use `#[serde(serialize_with = "...")]` or convert to epoch i64
- `ArchiveState`, `CardMissingState`, `CardRefreshState` — enums need Serialize derive too

**Minimal core changes needed:**
1. Add `serde::Serialize` derive to `DashboardCardViewModel` (behind `#[cfg_attr(feature = "bugsweeper", derive(serde::Serialize))]`)
2. Add `serde::Serialize` to `ArchiveState`, `CardMissingState`, `CardRefreshState` enums (same conditional)
3. Handle `SystemTime` field with a custom serializer or skip

## 4. Slint Property Access API

All properties on `DashboardWindow` have generated Rust accessors:
- Getters: `window.get_cards()`, `window.get_search_text()`, `window.get_active_mode_index()`, etc.
- Setters: `window.set_cards(...)`, `window.set_search_text(...)`, `window.set_active_mode_index(...)`, etc.
- Callbacks: `window.invoke_refresh_all_clicked()`, `window.invoke_card_refresh(idx)`, etc.

**There is NO dynamic property access by name.** All access is through the generated typed API. BUGSWEEPER must map string property names to the correct getter/setter call via a match statement.

**Full property inventory from dashboard.slint:**
- `cards: [CardData]` — the main data model
- `search-text: string` (in-out)
- `active-mode-index: int` (in)
- `toast-message: string` (in)
- `toast-visible: bool` (in-out)
- `search-focused: bool` (in-out)
- `connection-status: int` (in)
- `show-option-grid: bool` (in)
- `hidden-archived-count: int` (in)
- `current-show-archived: bool` (in)
- `settings-modal-open: bool` (in-out)
- `pending-edit-count: int` (in)
- Plus ~15 more settings/modal properties

**Full callback inventory (34 callbacks):**
- Core: `refresh-all-clicked`, `card-refresh(int)`, `card-save-note(int, string)`, `card-summary(int)`
- Navigation: `mode-switch-up`, `mode-switch-down`, `esc-pressed`, `tab-clicked(int)`, `tile-clicked(string)`
- Card actions: `card-archive(int)`, `card-unarchive(int)`, `card-save-rx-od(int, string)`, etc.
- Search: `search-changed(string)`, `chip-toggled(int)`
- Modal: `settings-clicked`, `pick-recipient-clicked(int)`, `lookup-search-changed(string)`

## 5. Window Handle Threading

**DashboardWindow is `!Send`** — cannot be moved to the HTTP server thread.

**Proven pattern in codebase (live_client.rs:70-78, main.rs:1070):**
```rust
let window_weak = window.as_weak();  // Weak<DashboardWindow> IS Send
// Pass window_weak to background thread
// Inside invoke_from_event_loop: weak.upgrade() → Option<DashboardWindow>
```

**For BUGSWEEPER:** The server startup function receives `slint::Weak<DashboardWindow>`. The weak reference is cloned into each request handler's `invoke_from_event_loop` closure.

## 6. SQLite Read-Only Access

**Current setup:** `SqliteStore` wraps `Arc<Mutex<Connection>>` with WAL mode.

**For BUGSWEEPER:** Open a second, independent read-only connection:
```rust
let conn = Connection::open_with_flags(
    db_path,
    OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_NO_MUTEX,
)?;
```

WAL mode allows unlimited concurrent readers. The BUGSWEEPER connection can read freely while the main app's connection writes. No locking contention.

**Alternative:** Share the existing `Arc<SqliteStore>` from `LiveClient::store()`. This is already `Arc<Mutex<Connection>>` — BUGSWEEPER would lock briefly for each read. Simpler but adds contention to the main store mutex.

**Recommendation:** Share the existing `Arc<SqliteStore>` — the reads are fast (< 1ms) and locking briefly is simpler than managing a second connection with its own migration state. The mutex contention is negligible for a debug tool.

## 7. Feature Gate Architecture

**Add to workspace Cargo.toml:**
```toml
[workspace]
members = [
  "crates/app",
  "crates/bugsweeper",  # new
  "crates/core",
  "crates/integrations",
  "crates/service",
]
```

**Add to crates/app/Cargo.toml:**
```toml
[dependencies]
bugsweeper = { path = "../bugsweeper", optional = true }

[features]
bugsweeper = ["dep:bugsweeper"]
```

**In main.rs (after window creation + state setup, ~10 lines):**
```rust
#[cfg(feature = "bugsweeper")]
{
    let bs_weak = window.as_weak();
    let bs_store = Arc::clone(&store);  // or from rx_write_handle
    let bs_runtime = runtime.clone();   // Need to make runtime Arc for this
    bugsweeper::start(bs_weak, bs_store, bs_config);
}
```

**Key constraint:** `runtime` is currently `Rc<RefCell<DashboardRuntime>>` — NOT Send. BUGSWEEPER cannot directly access it from the HTTP thread. Options:
1. Access runtime state ONLY through `invoke_from_event_loop` — read on UI thread, send back via channel
2. Change runtime to `Arc<Mutex<DashboardRuntime>>` — larger refactor, affects all callbacks
3. Have BUGSWEEPER capture runtime state queries as closures that run on the UI thread

**Recommendation:** Option 1 — all runtime queries go through `invoke_from_event_loop`. Zero refactoring of existing code. The pattern:
```rust
// BUGSWEEPER endpoint: GET /api/state/discovery
query_ui(&weak, |w| {
    // We can't access runtime directly, but we CAN read Slint properties
    // that reflect runtime state (active-mode-index, search-text, etc.)
    json!({
        "mode_index": w.get_active_mode_index(),
        "search_text": w.get_search_text().to_string(),
        "show_archived": w.get_current_show_archived(),
    })
})
```

For deeper runtime state not reflected in Slint properties: provide a callback on BUGSWEEPER init that captures the `Rc<RefCell<DashboardRuntime>>` and runs inside `invoke_from_event_loop`.

## 8. Validation Architecture

### Dimension 1: Happy Path
- Start app with `--features bugsweeper`, hit `/api/debug/health`, get 200 OK
- Read all cards via `/api/ui/cards`, get JSON array matching SQLite data
- Invoke callback via `/api/ui/callback`, observe state change

### Dimension 2: Error Paths
- Query when window is destroyed → graceful error JSON
- Invalid property name → 404 with available properties list
- Invalid callback args → 400 with expected signature
- Server port already in use → fallback port or clear error

### Dimension 3: Edge Cases
- Empty card model (no data loaded yet)
- Request during active sync cycle
- Rapid sequential requests (no race conditions)

### Dimension 4: Integration
- Agent workflow: health check → card dump → callback invoke → re-read state → verify change
- Screenshot tool captures visual state while BUGSWEEPER provides data state

### Dimension 5: Performance
- JSON serialization of full card model (50-100 cards) in < 100ms
- Property read round-trip (HTTP → UI thread → HTTP) in < 50ms
- No impact on app performance when no requests are in flight

---

*Phase: 16.2-bugsweeper*
*Research completed: 2026-03-24*
