---
phase: 16.1-discord-username-inline-editing
plan: "04"
subsystem: ui
tags: [discord, avatar, github-branch, slint, projection, wcm]

requires:
  - phase: 16.1-03
    provides: Slint CardData fields (discord-username, avatar-image, last-activity-label, last-activity-products, recipient-id), popover redesign, Discord username edit UI
  - phase: 16.1-01
    provides: HttpDiscordUserClient, fetch_and_cache_avatar, AvatarBranchClient, SqliteStore avatar/discord methods, AppConfig discord fields
  - phase: 16.1-02
    provides: dashboard.slint callbacks (card-save-discord-username, discord-save-token, discord-clear-token), properties (discord-token-configured, discord-token-text)

provides:
  - compute_last_activity function in projection.rs (status mapping with "Shipped"/"Returning" labels)
  - view_model_to_card_data populates discord_username, avatar_image (load_from_path), last_activity_label, last_activity_products, recipient_id
  - project_snapshot populates avatar_image_path (disk cache check) and last_activity fields
  - Free functions: write_discord_username_to_gh, resolve_discord_user_and_fetch_avatar, upload_avatar_to_branch, sync_avatars_from_branch
  - on_card_save_discord_username callback with optimistic UI + background GH write-back + Discord resolution + avatar fetch + branch upload
  - on_discord_save_token callback with WCM store + AppConfig persistence
  - on_discord_clear_token callback with WCM delete + AppConfig persistence
  - Startup: discord_token_configured check from WCM; sync_avatars_from_branch background thread
  - Bug fix: on_settings_save_clicked now preserves existing discord_bot_secret_ref instead of clobbering with None

affects: [Phase 17, Phase 18, Phase 19, live_client, projection pipeline]

tech-stack:
  added: []
  patterns:
    - "Free function pattern for background GH operations (write_discord_username_to_gh wraps write_rx_to_gh)"
    - "Two-tier avatar storage: local disk cache (performance) + GitHub branch (durable store)"
    - "invoke_from_event_loop for UI thread updates from background threads after avatar fetch"
    - "TDD: compute_last_activity tested with 8 unit tests covering all status mappings"
    - "Slint binding loop fix: Rectangle wrapper around TouchArea+HorizontalLayout in Flickable"

key-files:
  created: []
  modified:
    - crates/app/src/dashboard/projection.rs
    - crates/app/src/main.rs

key-decisions:
  - "compute_last_activity maps 'In Transit' -> 'Shipped' and 'Return In Transit' -> 'Returning'; 'In Transit' as display label is FORBIDDEN"
  - "on_settings_save_clicked preserves existing discord_bot_secret_ref/guild_id when saving other settings"
  - "sync_avatars_from_branch runs on background thread at startup only when discord_bot_secret_ref is configured"
  - "WCM set/delete require &mut self; on_discord_save_token and on_discord_clear_token both use let mut wcm"

patterns-established:
  - "compute_last_activity: status_display closure maps raw status strings to display labels"
  - "Startup token check: load WCM by secret_ref from config, set_discord_token_configured if present"

requirements-completed: [DSC-05, DSC-06, DSC-07, DSC-08, DSC-09, DSC-10, DSC-11, DSC-12, DSC-13]

duration: 35min
completed: 2026-03-23
---

# Phase 16.1 Plan 04: Discord Wiring and Projection Pipeline Summary

**End-to-end Discord username editing with GH write-back, avatar fetch via Discord CDN, two-tier storage (local cache + GitHub orphan branch), startup sync, WCM-backed token management, and compute_last_activity projection**

## Performance

- **Duration:** ~35 min
- **Started:** 2026-03-23T13:00:00Z
- **Completed:** 2026-03-23T13:35:00Z
- **Tasks:** 2 of 3 (Task 3 is a human-verify checkpoint)
- **Files modified:** 3

## Accomplishments

- Implemented `compute_last_activity` with correct status mapping (In Transit -> Shipped, Return In Transit -> Returning) with 8 passing unit tests
- Wired all Discord callbacks in main.rs: username save (optimistic UI + background GH + Discord resolution + avatar fetch + branch upload), token save/clear with AppConfig persistence
- Updated `view_model_to_card_data` to populate discord_username, avatar_image (via Image::load_from_path), last_activity_label, last_activity_products, recipient_id
- Fixed pre-existing Slint binding loop bug: TouchArea with `width: parent.width` inside HorizontalLayout in Flickable — replaced with Rectangle wrapper pattern in 3 locations

## Task Commits

Each task was committed atomically:

1. **Task 1: Projection pipeline** - `9d1874b` (feat)
2. **Task 2: Main.rs Discord wiring** - `38c3db8` (feat)

Task 3 is a human-verify checkpoint — execution paused.

## Files Created/Modified

- `crates/app/src/dashboard/projection.rs` - Added compute_last_activity, avatar_image_path population, last_activity fields
- `crates/app/src/main.rs` - Added 4 free functions + 3 callbacks + startup Discord init + bug fix

## Decisions Made

- `compute_last_activity` maps status strings literally: "In Transit" -> "Shipped" display label. The string "In Transit" as a display label is prohibited.
- `on_settings_save_clicked` now reads the existing config to preserve `discord_bot_secret_ref` and `discord_guild_id` when saving GitHub/Shopify settings.
- `sync_avatars_from_branch` uses `read_all_recipients()` (the actual method name in SqliteStore) rather than `list_recipients()` (which doesn't exist).

## Deviations from Plan

### Auto-fixed Issues

**1. [Rule 3 - Blocking] Slint binding loop in card.slint prevented compilation**
- **Found during:** Task 1 (first compile attempt)
- **Issue:** Three `TouchArea` elements with `width: parent.width` inside `HorizontalLayout` (discord username display row, OD row, OS row) caused binding loops: `width -> touch.width -> layoutinfo-h -> ... -> viewport-width -> width`
- **Fix:** Wrapped each display-mode section in a `Rectangle { height: 20px; }` so the TouchArea can reference `parent.width` on a non-layout container without circular dependency
- **Files modified:** `crates/app/ui/card.slint` (3 locations) — already committed from Plan 03
- **Verification:** `cargo check --workspace` passes; `cargo test -p app --lib` 154 tests pass
- **Committed in:** `02719dd` (from Plan 03 — file was clean but the fix was confirmed as already present)

**2. [Rule 1 - Bug] on_settings_save_clicked overwrote discord_bot_secret_ref with None**
- **Found during:** Task 2 (reading main.rs implementation)
- **Issue:** Settings save path hardcoded `discord_bot_secret_ref: None` and `discord_guild_id: None`, which would clear Discord configuration every time the user saved GitHub/Shopify settings
- **Fix:** Load existing config before building new config; preserve Discord fields with `existing_cfg.as_ref().and_then(...)` pattern
- **Files modified:** `crates/app/src/main.rs`
- **Verification:** Field preserved through save_config round-trip
- **Committed in:** `9d1874b` (Task 1 commit)

**3. [Rule 3 - Blocking] Plan used list_recipients() which does not exist in SqliteStore**
- **Found during:** Task 2 (reading sqlite.rs)
- **Issue:** Plan's sync_avatars_from_branch code called `store.list_recipients()` but the actual method is `store.read_all_recipients()`
- **Fix:** Used `read_all_recipients()` throughout sync_avatars_from_branch
- **Files modified:** `crates/app/src/main.rs`
- **Committed in:** `38c3db8` (Task 2 commit)

---

**Total deviations:** 3 auto-fixed (1 blocking compilation, 1 bug, 1 blocking API mismatch)
**Impact on plan:** All fixes necessary for correct compilation and behavior. No scope creep.

## Issues Encountered

- WCM `set`/`delete` require `&mut self` — plan code used immutable `wcm`; fixed to `let mut wcm`.
- Slint binding loop existed in card.slint from Plan 03 work; confirmed fixed in `02719dd`.

## User Setup Required

External services require manual configuration before Phase 16.1 end-to-end features work:
- **Discord Bot Token:** Discord Developer Portal -> Applications -> Bot -> Copy Token -> store via Settings modal
- **Discord Guild ID:** Set `discord_guild_id` in config.toml (the server/guild ID)
- **GitHub PAT scope:** Verify existing PAT has `repo` scope for Contents API (avatar branch operations)

## Next Phase Readiness

- Phase 16.1 Tasks 1-2 complete; Task 3 (human-verify) checkpoint reached
- All Rust-side callbacks wired; full end-to-end flow ready for UAT
- After human verification and approval, phase is complete

---
*Phase: 16.1-discord-username-inline-editing*
*Completed: 2026-03-23*
