mcp: project-plan — PBS Membership LOE 1 Phase 3c — Saved Recipes Detail Page
This commit is contained in:
parent
e799165d1b
commit
aebbc485c8
152
Sources/Dev/pbs-membership-loe1-phase3c-saved-recipes-detail.md
Normal file
152
Sources/Dev/pbs-membership-loe1-phase3c-saved-recipes-detail.md
Normal file
@ -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.
|
||||
Loading…
Reference in New Issue
Block a user