# Phase 18: GH Issues Write-Back, Notes, and Card Cloud Storage - Context

**Gathered:** 2026-03-27
**Status:** Ready for planning

<domain>
## Phase Boundary

Wire `ww-card` GH Issue creation/update for every recipient card, push notes as GH Issue comments, migrate PendingEditQueue from JSON file to SQLite, and deliver a hybrid PendingEditFlusher (immediate attempt + timer retry).

Requirements: CLOUD-01, CLOUD-02, CLOUD-05

</domain>

<decisions>
## Implementation Decisions

### ww-card Issue Body Schema
- **D-01:** Full snapshot body — includes identity AND mutable state fields. Rationale: GH Project rows represent recipients, not cards (shipments), so the GH Issue body is the only cloud location for card-level data.
- **D-02:** Body JSON format (consistent with `ww-product` pattern):
  ```json
  {
    "schema_version": 1,
    "card_id": "pkg_abc123",
    "recipient_key": "john-doe",
    "shopify_order_id": "6012345",
    "product_refs": [
      { "product_id": "prod_1", "serial_id": "HMD-001" }
    ],
    "shipment_status": "InTransit",
    "shipment_status_date": "2026-03-15"
  }
  ```
- **D-03:** Body must be updated when product_refs or shipment_status change (queued via PendingEditFlusher).
- **D-04:** Add `CardIssueBody` struct alongside existing `ProductIssueBody` / `ProductUnitIssueBody` in `issues_client.rs`, with `parse_card_body()` / `format_card_body()` helpers.

### Card-to-Issue Lifecycle
- **D-05:** Eager creation on first sync — any card in SQLite without a `github_issue_number` gets a `ww-card` issue created during startup sync. Every card always has a GH Issue.
- **D-06:** Issue title format: `{Recipient Name} - {First Product Name} {Unit Serial if Relevant} (+{Remaining Product Count}) [#{Order ID}]`
  - Example: `John Doe - Frying Pan FP3067 (+2) [#6012345]`
  - Title updated when products are added/removed from a card.
- **D-07:** Issue label: `ww-card` (consistent with `ww-product` / `ww-product-unit` labeling pattern).
- **D-08:** Add `github_issue_number: Option<i64>` column to `cards` table (new migration).

### Notes as GH Issue Comments
- **D-09:** Write-only sync direction for Phase 18 (app → GH). Design for future bidirectional by adding `synced_at` column to `notes` table.
- **D-10:** Backfill existing notes on issue creation — when creating a `ww-card` issue eagerly, post all existing notes as GH comments in chronological order (oldest first). Mark all as synced.
- **D-11:** GH comment format for notes:
  ```
  `ww-note`

  > _Note content here, italicized in a quote block._
  > _Multi-line notes preserve line breaks._
  ```
  The `` `ww-note` `` tag makes comments identifiable for future read-back parsing.
- **D-12:** Each note gets its own GH Issue comment. One comment = one NoteEntry.
- **D-13:** Add `synced_at: Option<String>` column to `notes` table (new migration). NULL = not yet synced. ISO 8601 timestamp when synced.

### PendingEditFlusher Design
- **D-14:** Hybrid flush strategy — try immediately on edit, queue failures to SQLite `pending_edits` table, timer retries every 60s with exponential backoff.
- **D-15:** Migrate from JSON file (`pending_edits.json`) to SQLite `pending_edits` table. No migration of existing entries needed — delete old file, start fresh.
- **D-16:** Edit types for GH write-back:
  - `SaveNote` → GH Issue comment creation
  - `UpdateCardBody` → GH Issue body edit (product_refs, shipment_status changes)
  - `UpdateCardTitle` → GH Issue title edit (product list changes)
- **D-17:** On flush failure, increment `retry_count` and apply exponential backoff (e.g., 60s, 120s, 240s, cap at ~15min).
- **D-18:** Flusher runs as a background timer alongside normal app operation. Non-blocking to UI.

### Claude's Discretion
- Exact backoff cap and retry limit for PendingEditFlusher
- Internal batching strategy (per-card grouping, ordering)
- Migration file numbering (next available V00X)
- Whether to coalesce multiple body updates into a single edit before flushing

</decisions>

<specifics>
## Specific Ideas

- Issue title should be descriptive enough to find in GitHub search: `John Doe - Frying Pan FP3067 (+2) [#6012345]`
- Note comments use `` `ww-note` `` code tag + italic quote block — clean in GitHub UI and machine-parseable for future read-back
- Full snapshot body because GH Project rows are recipients, not cards — the issue is the only cloud representation of a shipment/card

</specifics>

<canonical_refs>
## Canonical References

**Downstream agents MUST read these before planning or implementing.**

### Data architecture
- `.planning/DATA-FLOW.md` — Authoritative reference for all data entities, field sources, sync directions. Especially:
  - RULE-05 (GH Issues are cloud of record)
  - RULE-08 (`latest_note` deprecated, notes = `Vec<NoteEntry>` from GH Issue comments)
  - Layer 2 (Cards) field table — `notes`, `github_issue_id` fields
  - GH Issue Storage (`ww-card`) section — body structure
  - Write-Back Path section

### Existing GH Issues client
- `crates/integrations/src/github/issues_client.rs` — Phase 17 GhIssuesClient with create/edit/list/subissue methods. Extend for `ww-card`.

### Current pending edit infrastructure
- `crates/app/src/dashboard/edit_queue.rs` — Current JSON-file PendingEditQueue to be replaced
- `crates/service/src/db/sqlite.rs` — SQLite store with existing `pending_edits` table schema (unused)

### Notes model
- `crates/core/src/domain/note.rs` — NoteEntry struct
- `crates/service/src/db/sqlite.rs` — `save_note()`, `read_notes()` methods, `notes` table

### Card model
- `crates/app/src/service_client.rs` — RecipientCardSnapshot with `notes: Vec<NoteEntry>`
- `crates/app/ui/dashboard.slint` — CardData UI struct (has `note-preview`, no notes array)

### Requirements
- `.planning/REQUIREMENTS.md` — CLOUD-01, CLOUD-02, CLOUD-05 definitions

</canonical_refs>

<code_context>
## Existing Code Insights

### Reusable Assets
- **GhIssuesClient** (`issues_client.rs`): Has `create_issue(title, body, label)`, `edit_issue_body(number, body)`, `list_issues_by_label(label)` — directly reusable for `ww-card`
- **`parse_issue_body_json()` / `format_issue_body_json()`**: JSON code block parsing/formatting — reuse for `CardIssueBody`
- **`NoteEntry { date, content }`**: Already defined in `crates/core/src/domain/note.rs`
- **`pending_edits` SQLite table**: Schema already exists in V001 migration — needs to be wired into app code
- **`RecipientCardSnapshot.notes: Vec<NoteEntry>`**: Already wired in app layer projection

### Established Patterns
- **Issue body pattern**: JSON code block with `schema_version: 1`, parsed via serde_json — follow for `ww-card`
- **`gh` CLI transport**: All GH API calls go through `gh` CLI binary — no direct HTTP
- **Label-based issue typing**: `ww-product`, `ww-product-unit` → add `ww-card`
- **SQLite as single read source**: All UI reads from SQLite, write-back is async to cloud

### Integration Points
- **Startup sync**: Extend sync flow to check for cards missing `github_issue_number` and create issues
- **Note save path**: `save_package_note` → save to SQLite → queue GH comment → flush
- **Card mutation callbacks**: Product add/remove, status changes → queue body/title update → flush
- **PendingEditFlusher timer**: New background task alongside existing sync timer

</code_context>

<deferred>
## Deferred Ideas

- **Bidirectional note sync** (GH comments → app): Design for it now (`synced_at` column), implement in future phase
- **Product unit ownership audit trail via GH issue timeline comments**: Captured as todo, separate concern
- **Auto-promote recipient GH Project rows from drafts to ww-recipient issues**: Captured as todo, different entity type

</deferred>

---

*Phase: 18-gh-issues-write-back-notes-and-card-cloud-storage*
*Context gathered: 2026-03-27*
