6.6 KiB
| created | path | project | tags | type | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 2026-07-23 | Sources/Dev | pbs-membership-loe1-phase3c-saved-recipes-detail |
|
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.