From aebbc485c83b97eb3831aac6ad6d924fa5176925 Mon Sep 17 00:00:00 2001 From: Lovebug MCP Date: Thu, 23 Jul 2026 16:46:37 +0000 Subject: [PATCH] =?UTF-8?q?mcp:=20project-plan=20=E2=80=94=20PBS=20Members?= =?UTF-8?q?hip=20LOE=201=20Phase=203c=20=E2=80=94=20Saved=20Recipes=20Deta?= =?UTF-8?q?il=20Page?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...rship-loe1-phase3c-saved-recipes-detail.md | 152 ++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 Sources/Dev/pbs-membership-loe1-phase3c-saved-recipes-detail.md diff --git a/Sources/Dev/pbs-membership-loe1-phase3c-saved-recipes-detail.md b/Sources/Dev/pbs-membership-loe1-phase3c-saved-recipes-detail.md new file mode 100644 index 0000000..eca8343 --- /dev/null +++ b/Sources/Dev/pbs-membership-loe1-phase3c-saved-recipes-detail.md @@ -0,0 +1,152 @@ +--- +created: '2026-07-23' +path: Sources/Dev +project: pbs-membership-loe1-phase3c-saved-recipes-detail +tags: +- pbs +- membership +- loe1 +- garden +- flask +- javascript +- csrf +- wordpress +type: project-plan +--- + +# PBS Membership LOE 1 Phase 3c — Saved Recipes Detail Page + +## Constitution + +n/a — sub-phase of an existing LOE. Governed by 3a's settled architecture. + +--- + +## Spec + +### Goal + +The click-through destination from the 3b Saved Recipes seed-packet card. A member views their saved recipes, navigates through them, and can remove one. + +### Problem + +3b gives members a count and a clickable card with nowhere to go. 3c is the destination. + +### Out of scope + +- Filtering and sorting beyond save-date-descending +- Tombstone cards for unavailable recipes +- Audit/events history table +- Test coverage (deferred to end-of-LOE hardening pass) + +### User journey + +Logged-in Sunnie lands on `/the-garden/`, clicks the Saved Recipes packet, arrives at `/the-garden/saved`. Sees their saves newest-first, 12 per page. Pages through them. Unsaves one; the card disappears with a brief undo option. Clicks a recipe to read it on the main WordPress site. + +### Requirements + +- **FR-001** — WHEN a member with saved recipes opens `/the-garden/saved`, THE SYSTEM SHALL display saves newest-first, 12 per page. +- **FR-002** — WHEN a member clicks a page number, THE SYSTEM SHALL swap the grid without a full page reload and update the URL via `pushState`. +- **FR-003** — WHEN a member unsaves a recipe, THE SYSTEM SHALL hide the card immediately and present an undo affordance. +- **FR-004** — WHEN a member invokes undo within the window, THE SYSTEM SHALL leave the database unchanged. +- **FR-005** — WHEN the undo window expires, THE SYSTEM SHALL delete the interaction row. +- **FR-006** — WHEN a member has zero saved recipes, THE SYSTEM SHALL display the empty-state copy and a link to the recipes index. +- **FR-007** — WHEN a saved recipe is absent from WordPress, THE SYSTEM SHALL omit it from results and log the occurrence. +- **FR-008** — WHEN a recipe has no featured image, THE SYSTEM SHALL display the fallback thumbnail. +- **FR-009** — WHEN a delete request arrives without a valid CSRF token, THE SYSTEM SHALL reject it. +- **FR-010** — WHEN a delete targets an already-removed row, THE SYSTEM SHALL return success. +- **FR-011** — WHEN an unsave empties the current page, THE SYSTEM SHALL re-fetch or fall back to the previous page. +- **FR-012** — WHEN a delete request fails, THE SYSTEM SHALL leave the card in place and surface an error message. + +### Success criteria + +All FRs above observable in-browser. No full-page reloads during paging or unsaving. + +### Edge cases + +- Recipe deleted/unpublished in WP → skip and log +- No featured image → fallback asset +- Double-click unsave → idempotent success +- Emptied page after unsave → re-fetch or step back + +--- + +## Plan + +### Decisions + +| Decision | Chosen | Rejected | Reversibility | +|---|---|---|---| +| Unsave mechanism | Inline JS fetch, no reload | Form POST + redirect | Two-way, but sets the garden's first write pattern | +| Delete semantics | Hard delete | Soft delete (unique-constraint conflict on save/unsave toggle) | One-way for data already deleted | +| History | None now; audit/events table if ever needed | Soft-delete flag on live table | Two-way | +| Undo | Client-side optimistic; DELETE fires on window expiry | Delete-then-recreate (timestamp jump); server-side buffer (soft delete in costume) | Two-way | +| Thumbnail source | WP featured image | WPRM recipe image | Two-way; canonical for the garden going forward | +| Pagination style | Numbered pages, JS-swapped grid, `pushState` | Load-more (hides collection shape); plain links (full reload); infinite scroll | Two-way | +| Page size | 12 | 24 | Two-way — one constant | +| Paging query | OFFSET | Cursor-based (unnecessary without load-more) | Two-way | +| CSRF | Built now | Deferred to design lock — rejected; unprotected write endpoint goes live on deploy | One-way once deployed | +| Tests | Deferred to end-of-LOE | Per-phase — rejected; pattern not yet proven in browser | Two-way | + +### Data model + +Inherits 3a. Reads `pbs_garden.user_interactions`, joined read-only against WordPress tables for title, slug, and featured image. No schema changes. + +### Interfaces / contracts + +Two JSON endpoints under the blueprint (no prefix stripping): + +- **Read** — returns a page of saves, newest-first, 12 per batch, with total/page metadata for the numbered links. No CSRF (read-only). +- **Delete** — removes one interaction row. Requires valid CSRF token in header. Returns `{"ok": true}`; idempotent on already-deleted rows. + +CSRF token embedded in the page, read by JS, sent as a request header, validated server-side before the DB call. **This is the garden's first write pattern — all future garden writes inherit it.** + +### Deployment + +Inherits 3a — existing `pbs-garden` container, Traefik routing, no new infrastructure. + +### Tests + +Deferred to an end-of-LOE hardening pass. The one test worth writing then: CSRF rejection path, since it fails silently under refactor. + +### NFRs + +Latency/observability n/a at this scale beyond the FR-007 log line. + +--- + +## Tasks + +### Phase 3c scope + +- Read endpoint + paging (FR-001, FR-002, FR-011) +- Grid template, thumbnails, fallback (FR-008) +- Empty state (FR-006) +- Delete endpoint + CSRF guard (FR-009, FR-010, FR-012) +- Unsave + undo JS (FR-003, FR-004, FR-005) +- WP-absence skip and log (FR-007) + +### Iterate-shaped + +**Undo timing and toast behavior** — the 5-second window and affordance are a starting point, not a spec. Converge with Travis in-browser; done when it feels right. + +### Human-required handoffs + +- Fallback thumbnail asset (needs to be made) +- Jenny's sign-off on empty-state copy: *"Pantry's empty. Save a recipe and we'll stock these shelves."* — paired with chef-hat Sunnie and a link to the recipes index + +--- + +## Cross-cutting + +### Codebase assumptions + +3a shipped: standalone `pbs-garden` container, `pbs_garden` DB, Option D chrome, WordPress-validates-own-cookie auth, blueprint owning `/the-garden` with no prefix stripping. 3b shipped: the saved-recipes card links here. + +### Unresolved questions + +**Defer and tag:** exact undo window duration; fallback thumbnail visual treatment. + +### Pre-mortem + +Most likely failure is the CSRF wiring — new plumbing, and a misconfigured token check fails quietly or blocks every delete with an unhelpful error. Second: the emptied-page-after-unsave case, easy to miss until a member lands on an empty page 2. Third: paging plus `pushState` plus in-place unsave means grid state lives in three places; back-button behavior after an unsave is the likely rough edge. \ No newline at end of file