107 lines
12 KiB
Markdown
107 lines
12 KiB
Markdown
---
|
|
created: '2026-06-19'
|
|
path: Sources/Dev
|
|
project: pbs-membership-platform-overview
|
|
tags:
|
|
- pbs
|
|
- membership
|
|
- loe1
|
|
- phase-1
|
|
- phase-2
|
|
- phase-3a
|
|
- flask
|
|
- wordpress
|
|
- docker
|
|
- ansible
|
|
- garden
|
|
type: session-notes
|
|
---
|
|
|
|
## Outcome
|
|
|
|
Three phases of the PBS Sunflower Garden are now coded, pushed to gitea, and deployed end-to-end on the herbylab sandbox VM (`pbs.herbylab.dev`, PVE 202, IP `10.0.21.101`). The full auth chain (browser cookie → Traefik → member-garden → GardenAuth resolver → WordPress mu-plugin → identity) is verified working over the public route. The Garden frontend renders, the API gates correctly, the deploy is repeatable.
|
|
|
|
**Phases executed in one session (2026-06-19), Lovebug supervising 6 workers via petal-dispatch:**
|
|
|
|
1. **Phase 1 finish (member-garden functional tier + WP mu-plugin + DB apply + integration deploy)** — extends thread #597
|
|
2. **Phase 2 (save-button WPCode snippet + member-garden public Traefik routing)** — thread #600
|
|
3. **Phase 3a (Garden skeleton at `/the-garden/`)** — thread #601
|
|
|
|
What ships:
|
|
- member-garden Flask app with four interaction endpoints (POST/DELETE/GET/check), pluggable `AuthResolver` (WP-cookie impl, 300s bounded TTL cache, fail-closed on missing shared secret), Garden blueprint at `/the-garden` with branded shell (header/hero/card-grid/footer), logout + login routes.
|
|
- WordPress mu-plugin (`/wp-json/garden/v1/validate`) protected by two-belt defense: shared-secret header `X-Garden-Auth` + Traefik path deny for `/wp-json/garden/*`.
|
|
- wordpress-install ansible role wiring: clone member-garden from gitea (configurable branch via `member_garden_repo_version`), bind-mount mu-plugin into WP, template the shared secret into both containers' env, force-rebuild member-garden image on every clone, route `/api/interactions` and `/the-garden` to member-garden via Traefik.
|
|
- WPCode Lite snippet source-of-truth in `wordpress-install/wpcode-snippets/save-button/` (manual paste into WP admin).
|
|
- Vault keys generated for the herbylab sandbox: `vault_garden_auth_shared_secret`, `vault_pbsgarden_db_password`. Staging/prod vaults untouched.
|
|
|
|
Branches landed (all gitea-only, never GitHub):
|
|
- `petal-power/member-garden`:
|
|
- `feat/phase-1-functional-tier` (commits `1820943`, `edf13ea`) — 34 tests, 99% coverage
|
|
- `feat/phase-3a-garden-skeleton` off the above (commits `7094076`, `c2097fa`) — 44 tests, 99% coverage
|
|
- `petal-power/wordpress-install`:
|
|
- `feat/wp-mu-plugin-garden-auth` (commit `116a72d`)
|
|
- `integration/garden-phase1-deploy` (tip `ca2bd35`) — merges fix-pbs-init-sql-syntax + mu-plugin branch + role bumps
|
|
- `feat/save-button-phase2` (tips `0ca8c1d` → `89428c6` → `4f2d105`) — save-button source + Traefik labels + Phase 3a inventory bump
|
|
|
|
Final smoke test on `https://pbs.herbylab.dev/`: 6/6 (deploy worker) + 4/4 (Phase 2 deploy worker) + 7/7 (Phase 3a deploy worker) = **17/17 checks ✅**.
|
|
|
|
## Topics Covered
|
|
|
|
- **Auth contract implementation** — both sides of the WP-validates-its-own-cookie design (vault 247/294 Option 2). Shared-secret header `X-Garden-Auth` was added during implementation as a server-to-server access control on top of the cookie validation, fails closed both sides on missing secret. Cache is 300s TTL, sha256-keyed, size-capped at 10k, positive-only (negative responses not cached).
|
|
- **Pluggable `AuthResolver` ABC** so a future React Native client can swap a token-based resolver in without touching endpoints.
|
|
- **`pbs_garden` DB applied to herbylab MySQL** via the existing scoped-user SQL pattern; grants verified: full on `pbs_garden.*`, SELECT on `wordpress.wp_posts`, USAGE elsewhere.
|
|
- **Traefik routing strategy for member-garden** — switched the originally-planned `garden.<domain>` subdomain to same-host path-prefixes (`/api/interactions` priority 110, `/the-garden` priority 100). Same-host is the only way the browser auto-sends the WP cookie to member-garden.
|
|
- **WPCode Lite save-button widget** — JS+CSS+README in repo as source of truth; copy/paste into WP admin. Reads post_id from `body.postid-{N}` (canonical WP, not the WPRM recipe id), detects login from `wordpress_logged_in_` cookie name (render hint; API 401 is authoritative), two injection points (top-of-post next to Jump/Print, inside recipe card next to Print). Logged-out → modal/bottom-sheet signup funnel.
|
|
- **Garden frontend** at `/the-garden/`: branded chrome with garden-owned palette, welcome hero with display-name greeting, 4-card grid (Saved Recipes active + 3 coming-soon), Jinja `_card.html` macro, `before_request` auth gate returns 302 (not 401) to UM login with `redirect_to=`. Static assets and `/logout`/`/login` are auth-exempt. UM_LOGIN_URL defaults to `${WP_BASE_URL}/login/`.
|
|
|
|
## Key Learnings
|
|
|
|
- **a-review is paying dividends.** Each worker invoked a-review (Gemini via `agy`) after zero-check. It caught: a TTL cache memory-leak/DoS vector on the resolver (size-capped + negative-not-cached), static assets behind the Garden auth gate (login loop), env-var default loaded before config overrides (silent prod-wrong default), button race during init API call (visible UI bug), and a cookie-name substring false-positive in the save-button JS. All real bugs the workers fixed before reporting done.
|
|
- **The `/api` Traefik collision was a near-miss.** pbs-api already owned `Host && PathPrefix(/api)` at priority 100. A literal `/api` member-garden router would have non-deterministically intercepted pbs-api's `/api/recipes`, `/api/health`, etc. Scoping to `/api/interactions` at priority 110 sidesteps it cleanly. **Rule worth codifying:** any new container claiming a path prefix on a shared host must check existing routers first; never claim a parent prefix of something already in use.
|
|
- **`docker_compose_v2` doesn't rebuild on source changes by default** — the first deploy of the functional tier silently shipped foundation-tier code because the existing image was cached. Fixed by adding `container_compose_build: always` for member-garden. Worth checking whether pbs-api has the same latent gap.
|
|
- **WP-CLI is not in the wordpress image.** Both deploy workers (W3, W6) had to mint test cookies by bootstrapping `wp-load.php` via `docker exec wordpress php -r '...'` and calling `wp_generate_auth_cookie` directly. Worth a follow-up to bake wp-cli into the image (or document the bootstrap pattern).
|
|
- **Same-host cookie routing is load-bearing.** The original spec (vault 247) mentioned a `garden.<domain>` subdomain as one option; that path silently breaks the browser sending the WP cookie to the Garden. Same-host path-prefix routing is the only working topology and should be documented as required, not optional.
|
|
- **WordPress on herbylab was never fully installed** before this session — the install wizard was reachable but no admin user existed. W3 ran `wp core install` to enable end-to-end auth verification. The admin password is ephemeral (not persisted to vault).
|
|
|
|
## Follow-ons
|
|
|
|
### Production-readiness gaps (must address before live cutover)
|
|
|
|
- [ ] **CSRF nonce on save/unsave** — flagged CRITICAL by a-review on W4. POST/DELETE to `/api/interactions` rely on ambient cookie auth with no nonce. Robust fix: add `X-WP-Nonce` validation to the member-garden API (`wp_create_nonce('wp_rest')` injected via WPCode into the page, JS sends it in header, Flask validates against WP). Current mitigations (same-origin + JSON content-type) are partial only.
|
|
- [ ] **member-garden migrations not automated** — `flask db upgrade` had to be run manually via `docker cp` of the migrations/ dir + container exec. Bake migrations into the image OR add an ansible task that runs them on deploy.
|
|
- [ ] **`X-Forwarded-Proto` not honored** — Traefik terminates TLS; Flask sees `http://` and builds the unauth 302 `redirect_to` with the wrong scheme. Fix with `werkzeug.middleware.proxy_fix.ProxyFix` in the app factory.
|
|
- [ ] **WP logout confirmation nonce** — `wp-login.php?action=logout` without `_wpnonce` shows the "Do you really want to log out?" interstitial. Seamless logout needs a WP-side endpoint that accepts authenticated logout without a nonce, or a mu-plugin route.
|
|
- [ ] **WPCode save-button browser test** — sandbox WP has no recipe content; W4 documented a manual test plan in the README but a real browser test (Chrome on a real recipe page) hasn't been done.
|
|
- [ ] **Brand color hex + Sunnie SVG assets** for both the save-button (filled saved state) and the Garden hero/card placeholders — Jenny input needed before launch.
|
|
|
|
### Carryover / side-issues (named, not fixed)
|
|
|
|
- [ ] **pbs-api crash-loop on herbylab** — `pbs_recipes` FK type mismatch (`instagram_posts_ibfk_1` references `pbs_recipes(post_id)` with incompatible `pbs_post_id` column type → MySQL `OperationalError 3780`). Does NOT touch `pbs_garden`. Travis to backfill (he mentioned this during the session).
|
|
- [ ] **Two unmerged branches in wordpress-install awaiting Travis's merge to main:** `fix-pbs-init-sql-syntax` (from thread #582) and `feat/wp-mu-plugin-garden-auth` (this session). Both currently included transitively via `integration/garden-phase1-deploy` and `feat/save-button-phase2`. Eventual merge order: `fix-pbs-init-sql-syntax` → main, then `feat/wp-mu-plugin-garden-auth` (rebased) → main, then `integration/garden-phase1-deploy` → main, then `feat/save-button-phase2` → main.
|
|
- [ ] **member-garden branches awaiting merge to main:** `feat/phase-1-functional-tier` first, then `feat/phase-3a-garden-skeleton` on top.
|
|
- [ ] **WP admin password on herbylab is ephemeral.** Document or store in herbylab vault if persistence is wanted.
|
|
- [ ] **`/login` vs auth-gate URL consistency** — Garden's convenience `/login` route hardcodes `wp-login.php` per the W5 brief, while the gate uses `UM_LOGIN_URL`. Unify on `UM_LOGIN_URL` in Phase 3b.
|
|
|
|
### Open questions for Jenny
|
|
|
|
- Final brand palette hex values (`--garden-bg`, `--garden-fg`, `--garden-accent`, `--garden-muted`, `--garden-card`, `--pbs-brand` for the save button).
|
|
- Real chef Sunnie SVG (hero placeholder), seed-packet active + coming-soon SVGs (card placeholders), filled Sunnie save-state SVG (button placeholder).
|
|
- Hero copy: "Welcome to the Sunflower Garden, {name}." / "Here's what's growing today." — keep or revise?
|
|
- Card lineup for launch: 3a ships Saved Recipes active + Meal Plans / Diversity Tracker / Profile coming-soon. Right set/labels?
|
|
- Top-nav placeholders: which LOE landing pages get permanent top-nav slots in Phase 3a vs. later.
|
|
- Pinterest button — decide whether Pin sits alongside Save (deferred per original spec).
|
|
|
|
### Phase 3b candidates (next session)
|
|
|
|
- Wire Saved Recipes card to real data: query `pbs_garden.user_interactions` joined with `wordpress.wp_posts`, show count + clickable.
|
|
- Build the saved-recipes detail page (3c) at `/the-garden/saved` (currently the 3b stub).
|
|
- "Recently added to your garden" strip on the home (Phase 3d).
|
|
|
|
## Architecture decisions worth keeping
|
|
|
|
- **Auth contract is the seam.** Mu-plugin returns `{authenticated, user_id, username, display_name, email, roles[]}`. Tiers ride in as additional WP role values (Path A — no second source of truth). Multi-tier supported from day one without a contract change.
|
|
- **Two-belt internal-only on the mu-plugin.** Shared-secret header (PHP `permission_callback` + `hash_equals`) + Traefik deny router (`PathPrefix(/wp-json/garden)` → 403). Belt #1 alone would suffice, but belt #2 protects against env-var leak scenarios.
|
|
- **`AuthResolver` ABC pattern.** Future React Native app slots in a `TokenResolver` (bearer token) without changing endpoints; the protocol returns the same `AuthIdentity` regardless of source.
|
|
- **300s in-process TTL cache on the resolver.** Keyed by `sha256(cookie_value)`, size-capped at 10k, positive-only. Cookie itself invalidates on WP logout (no manual cache busting needed). Per-process is fine — sessions don't need to be coherent across replicas at the auth layer.
|
|
- **Same-host path-prefix routing.** `pbs.herbylab.dev/api/interactions/...` and `pbs.herbylab.dev/the-garden/...` route to member-garden; the browser auto-sends the WP cookie. Avoids the subdomain trap.
|
|
- **Integration branch as the deploy target.** `integration/garden-phase1-deploy` merges in-flight feature branches before they're merged to main, so the test VM can run them while Travis reviews PRs at his pace. Trade-off: integration branch needs to be kept current as new features land. |