wiki-vault/Sources/Dev/pbs-membership-loe1-phase3c-saved-recipes-detail.md

6.6 KiB

created path project tags type
2026-07-23 Sources/Dev pbs-membership-loe1-phase3c-saved-recipes-detail
pbs
membership
loe1
garden
flask
javascript
csrf
wordpress
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.