Public Access
feat(ui): explicit Deny semantics with 2-denial hard-filter escalation (Sprint 8)
User policy decision (2026-06-05, exact): 'Hard filter. If it is denied
this week twice, it should be considered denied for good.'
The planner had no cross-week memory of denials: a denial on
meal_plan_item.approval_status was never consulted by the planner,
and NeverSuggest (the per-family permanent blocklist) was empty for
the user. The 'Roasted Sweet Potato and Chickpea Bowl' the user
denied on 2026-05-15 was still in the planner's pool 3 weeks
later.
Implements C + Z (explicit two-button model + soft-decay +
hard-filter escalation):
- Approve: untouched.
- Deny this week (1st in 90d): denial_expires_at = now() + 90d.
- Deny this week (2nd in 90d, server-side auto-escalation):
denial_expires_at = NULL + a NeverSuggest row written.
- Never again (explicit): same as the 2nd-time auto-escalation.
Both soft and permanent denials are hard filters in the planner
(per user). A denied recipe never reappears until either the 90d
window expires or the user un-blocks via the NeverSuggest API.
Changes:
- Migration 0016: meal_plan_item.denial_expires_at (partial index)
and meal_plan_vote.denial_scope.
- 3 backend helpers (_apply_denial, _ensure_never_suggest_recipe,
_has_prior_active_soft_denial) — single source of truth for the
deny path.
- POST /api/meals/items/{id}/deny?scope=this_week|never_again
(default this_week). Returns promoted_to_permanent.
- POST /api/meals/vote/{id} extended: vote=approve|deny|never_again.
Returns denial_scope + promoted_to_permanent.
- GET /api/meals/vote/{id} HTML page renders 3 buttons; supports
one-click ?scope=... for email direct-action links.
- Email template (step_email): 3 direct-action links per recipe
plus a secondary 'open vote page' link.
- Planner: _load_blocklists returns 3 sets; soft_denied_recipes
is hard-filtered (union with blocked_recipes at the call site).
- Frontend: MealCard renders 3 buttons (Approve / Deny this week
/ Never again) for pending items. handleDeny is scope-aware;
toast reflects promoted_to_permanent. window.confirm on
'Never again' prevents accidental permanent blocks.
Verification:
- npm run build green.
- 21/21 planner tests pass (1 pre-existing test_filter_blocks_by_cost
failure is NOT introduced by Sprint 8 — verified via git stash).
- Review/sprint8-verification.md: 11-step browser smoke + 4 API
curls + email-render procedure + rollback.
Files:
- backend/alembic/versions/0016_denial_decay_and_scope.py (new)
- backend/app/models/__init__.py:221-242, 250-269
- backend/app/schemas/__init__.py:204-219, 248-269
- backend/app/api/meals.py:30-138 (helpers), 240-330 (HTML page),
380-455 (submit_vote), 486-552 (deny_meal_item)
- backend/app/services/orchestrator/steps.py:283-300
- backend/app/services/planner/generate.py:59-99, 150-194
- frontend/src/api/index.ts:48-58
- frontend/src/pages/Dashboard.tsx:38-50, 385-410
- Review/{sprint8-verification,ui-nielsen-audit,handoff-ui-audit}.md
- fix-ui-audit.md
- docs/HANDOFF.md
- .agent/{plan,context}.md
Deploy (user runs on deployment host):
cd ~/MealPlanner && git pull
docker compose exec backend alembic upgrade head
docker compose -f docker-compose.yml up -d --build backend frontend
This commit is contained in:
@@ -2,7 +2,30 @@
|
||||
|
||||
You are taking over a 6-sprint UI/UX audit and fix cycle. All code changes are committed and build green. The user's deployment host (Tailscale `100.108.224.12`) is the only environment you should touch for verification — the local repo on this machine (`/home/peter/Projects/MealPlanner`) was the editing host; the running app lives elsewhere.
|
||||
|
||||
**Date of handoff: 2026-06-04. Last updated: 2026-06-05 (Sprint 7 in progress).**
|
||||
**Date of handoff: 2026-06-04. Last updated: 2026-06-05 (Sprint 8 in progress).**
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Active sprint: Sprint 8 — "Deny" semantics (C + Z, hard-filter escalation)
|
||||
|
||||
**Status: in progress. User approved on 2026-06-05. Code not yet committed.**
|
||||
|
||||
**User policy decision (2026-06-05, exact words):** "Hard filter. If it is denied this week twice, it should be considered denied for good."
|
||||
|
||||
**Root cause (one-liner):** the planner has no cross-week memory of denials. Denials live on the `meal_plan_item` row, are never consulted by the planner, and the `NeverSuggest` blocklist is empty for the user's family. The user's "Roasted Sweet Potato and Chickpea Bowl" was denied on 2026-05-15 but the recipe was still in the pool for the next 90+ days.
|
||||
|
||||
**Policy (Sprint 8):**
|
||||
|
||||
- "Approve" → `item.approval_status = approved`. n/a.
|
||||
- "Deny this week" (1st in 90d) → `denied` + `denial_expires_at = now() + 90d`. Recipe becomes eligible again after 90d.
|
||||
- "Deny this week" (2nd in 90d — **server-side auto-escalation**) → `denied` + `denial_expires_at = NULL` + a `NeverSuggest` row written. Permanent.
|
||||
- "Never again" (explicit) → same as the 2nd-time auto-escalation. Permanent.
|
||||
|
||||
**Scope (12 boxes):** see `.agent/plan.md` "Active sprint" section. Code changes are M-L: 1 migration, 2 model columns, 2 schema fields, 3 backend helpers, 2 endpoint extensions, 1 planner update, 1 email template update, 1 webui MealCard update. **No new dependencies. Migration 0016 required.**
|
||||
|
||||
**Tracking docs:** `Review/sprint8-verification.md` (deploy + smoke), `Review/ui-nielsen-audit.md` Sprint 8 status block, `fix-ui-audit.md` T2.1–T2.10, `Review/handoff-ui-audit.md` (this file), `docs/HANDOFF.md` Sprint 8 section.
|
||||
|
||||
**Thread 3 (§Future backlog) is deferred** until S8 is deployed + verified. F1 onboarding, F8/F9 proposals, dead `Generate Meal Plan` CTA at `Dashboard.tsx:415`.
|
||||
|
||||
---
|
||||
|
||||
@@ -278,4 +301,4 @@ cd frontend && npm run build
|
||||
|
||||
Trust the build output. Trust the smoke checklist. Don't trust the deployment host's UI until the user confirms. The verification model is "I shipped, you verified, you reported, I fixed" — the agent in this role never sees the live UI directly.
|
||||
|
||||
**Last updated: 2026-06-05** — Sprints 1, 2, 3, 4, 5, 6 deployed-or-awaiting-deploy; **Sprint 7 (webui empty-meal-plan fix) in progress**. See the "Active sprint" callout at the top of this file for the current state.
|
||||
**Last updated: 2026-06-05** — Sprints 1, 2, 3, 4, 5, 6 deployed-or-awaiting-deploy; Sprint 7 (webui empty-meal-plan fix) **committed `09c7525` awaiting user deploy**; **Sprint 8 (Deny semantics C + Z with hard-filter escalation) in progress**. See the "Active sprint" callout at the top of this file for the current state.
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
# Sprint 8 — Verification
|
||||
|
||||
**Sprint:** "Deny" semantics — explicit two-button model with 90-day decay and hard-filter escalation.
|
||||
**User policy decision (2026-06-05):** "Hard filter. If it is denied this week twice, it should be considered denied for good."
|
||||
**Status:** code complete, awaiting deploy.
|
||||
**Date of handoff:** 2026-06-05.
|
||||
|
||||
---
|
||||
|
||||
## Policy (recap)
|
||||
|
||||
| Action | Backend behavior | Decay |
|
||||
|---|---|---|
|
||||
| **Approve** | `item.approval_status = approved` | n/a |
|
||||
| **Deny this week** (1st time in 90d for this recipe) | `denied`, `denial_expires_at = now() + 90d` | after 90d, recipe is eligible again |
|
||||
| **Deny this week** (2nd time in 90d for this recipe — auto-escalation) | `denied`, `denial_expires_at = NULL`, **and** a `NeverSuggest` row is inserted for `(family_profile_id, recipe_id)` with `reason = "dislike"` | never (until user un-blocks) |
|
||||
| **Never again** (explicit) | same as the 2nd-time auto-escalation: `denied`, `denial_expires_at = NULL`, **and** a `NeverSuggest` row | never |
|
||||
|
||||
The 2-denial escalation is **server-side and atomic** — the API checks for a prior `denied` row with `denial_expires_at > now()` for the same `(family_profile_id, recipe_id)` before deciding whether to insert a `NeverSuggest` row. No client-side double-counting.
|
||||
|
||||
The planner's `_load_blocklists` now returns 3 sets; the soft-denied set is **hard-filtered** (per the user's decision — same as the permanent blocklist) so a denied recipe is never re-proposed until either the 90d window expires or the user un-blocks via the `NeverSuggest` API.
|
||||
|
||||
---
|
||||
|
||||
## What changed (recap)
|
||||
|
||||
| Layer | File | Change |
|
||||
|---|---|---|
|
||||
| Migration | `backend/alembic/versions/0016_denial_decay_and_scope.py` (NEW) | Adds `meal_plan_item.denial_expires_at TIMESTAMPTZ NULL` and `meal_plan_vote.denial_scope VARCHAR(16) NULL`. Partial index on `denial_expires_at` for fast lookup. |
|
||||
| Model | `backend/app/models/__init__.py:221-242, 250-269` | Two new columns. |
|
||||
| Schema | `backend/app/schemas/__init__.py:204-219, 248-269` | `MealPlanItemResponse.denial_expires_at`, `VoteRequest.denial_scope`, `VoteResponse.denial_scope`. |
|
||||
| Backend | `backend/app/api/meals.py:30-138` | 3 new helpers: `_apply_denial`, `_ensure_never_suggest_recipe`, `_has_prior_active_soft_denial`. |
|
||||
| Backend | `backend/app/api/meals.py:510-552` | `deny_meal_item` accepts `?scope=this_week\|never_again`; returns `promoted_to_permanent`. |
|
||||
| Backend | `backend/app/api/meals.py:380-455` | `submit_vote` handles `vote: "never_again"`; calls `_apply_denial` for both deny scopes; returns `denial_scope` + `promoted_to_permanent`. |
|
||||
| Backend | `backend/app/api/meals.py:240-330` | `get_vote_page` (HTML) now renders 3 buttons and supports a one-click `?scope=...` path for email direct-action links. |
|
||||
| Backend | `backend/app/services/orchestrator/steps.py:283-300` | Email template: 3 direct-action links per recipe (Approve / Deny this week / Never again), each a one-click GET to the vote page. |
|
||||
| Planner | `backend/app/services/planner/generate.py:59-99` | `_load_blocklists` returns 3 sets; soft-denied set is hard-filtered. |
|
||||
| Planner | `backend/app/services/planner/generate.py:150-194` | Call site updated; `rejected_summary` adds `soft_denied_recipe` diagnostic bucket. |
|
||||
| Frontend | `frontend/src/api/index.ts:48-58` | `meals.denyItem(itemId, { scope })`. |
|
||||
| Frontend | `frontend/src/pages/Dashboard.tsx:38-50, 385-410` | `MealCard` accepts scope-aware `onDeny`; renders 3 buttons (Approve / Deny this week / Never again) for pending items. `handleDeny` is scope-aware; toast reflects `promoted_to_permanent`. |
|
||||
| Docs | `Review/ui-nielsen-audit.md` + `fix-ui-audit.md` + `Review/handoff-ui-audit.md` + `docs/HANDOFF.md` + `.agent/plan.md` + `.agent/context.md` | Sprint 8 status blocks. |
|
||||
|
||||
**No new dependencies. Backend migration is required. Frontend + backend rebuild required.**
|
||||
|
||||
---
|
||||
|
||||
## Deploy commands
|
||||
|
||||
Run on the deployment host (`100.108.224.12`):
|
||||
|
||||
```bash
|
||||
# 1. Pull
|
||||
cd ~/MealPlanner
|
||||
git pull
|
||||
|
||||
# 2. Migration 0016 (adds denial_expires_at + denial_scope)
|
||||
docker compose exec backend alembic upgrade head
|
||||
|
||||
# 3. Rebuild backend + frontend
|
||||
docker compose -f docker-compose.yml up -d --build backend frontend
|
||||
```
|
||||
|
||||
**Order matters.** Pull → migrate → rebuild. The migration is forward-only and non-destructive (adds two NULL columns + one partial index).
|
||||
|
||||
---
|
||||
|
||||
## Smoke checklist (browser, on `http://100.108.208.56:8082/`)
|
||||
|
||||
| # | Action | Expected |
|
||||
|---|---|---|
|
||||
| 1 | Open the Dashboard. Find a pending meal (e.g. Chicken Fajitas on Mon 2026-06-08). | The card now shows 3 buttons: `Approve` (green), `Deny this week` (red), `Never again` (red, dashed border). |
|
||||
| 2 | Click `Deny this week` on a meal that has NOT been denied before. | Toast: "Denied this week — will not reappear for 90 days". Card status badge updates to `denied`. |
|
||||
| 3 | In a new browser tab, navigate to `/api/meals/items/<that-item-id>` (or use a plan regen to see the planner skip it). | The denied recipe does NOT appear in the next plan if a regen is triggered (hard filter). |
|
||||
| 4 | Manually set `denial_expires_at` to NULL on the first item, OR re-run the orchestrator's generate after a recipe rotation. The denied recipe's `denial_expires_at` is now NULL. | The recipe is in the `NeverSuggest` table. The planner will never propose it. |
|
||||
| 5 | Click `Deny this week` on a different recipe that has no prior soft denial. | Toast: "Denied this week — will not reappear for 90 days". The new recipe joins the soft set. |
|
||||
| 6 | Click `Deny this week` again on the SAME recipe (use the API to find a second item with the same `recipe_id`, e.g. by re-running the planner or by creating a second plan with the same recipe). | Toast: "Denied — won't suggest again (denied twice recently)". `NeverSuggest` table now has a row for this recipe. |
|
||||
| 7 | Click `Never again` on a fresh recipe. | Confirm dialog appears: "Never suggest '<name>' again? This permanently blocks the recipe for your family." Click OK. Toast: "Denied — will never be suggested again". |
|
||||
| 8 | Check the API: `GET /api/never-suggest?family_profile_id=...` | The new row appears in the list. |
|
||||
| 9 | Reload the page. | The 3 buttons are gone from cards whose `approval_status` is now `denied` (Sprint 8: only `pending` items show the buttons). Approved/denied items show the badge only. |
|
||||
| 10 | Open `/meals/<item-id>` (MealDetail). | No voting UI here — voting is via the email or the dashboard card (per the existing design). |
|
||||
| 11 | Open `/shopping-list` or `/pantry`. | No regression. The bulk pantry add (Sprint 6) and other features unaffected. |
|
||||
|
||||
---
|
||||
|
||||
## API smoke (curl, on the deployment host)
|
||||
|
||||
```bash
|
||||
# Find a pending item in the current plan
|
||||
PLAN_ID=$(curl -s "http://100.108.208.56:8082/api/meals?week_start=2026-06-08" | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])")
|
||||
ITEM_ID=$(curl -s "http://100.108.208.56:8082/api/meals/$PLAN_ID" | python3 -c "import sys,json; d=json.load(sys.stdin); print([i['id'] for i in d['items'] if i['approval_status']=='pending'][0])")
|
||||
echo "plan=$PLAN_ID item=$ITEM_ID"
|
||||
|
||||
# Test 1: deny with default scope (this_week)
|
||||
curl -s -X POST "http://100.108.208.56:8082/api/meals/items/$ITEM_ID/deny" | python3 -m json.tool | head -20
|
||||
# Expected: item.approval_status="denied", item.denial_expires_at ~ 90 days from now,
|
||||
# promoted_to_permanent=false (no prior denial)
|
||||
|
||||
# Test 2: never_again on a fresh item
|
||||
ITEM2=$(curl -s "http://100.108.208.56:8082/api/meals/$PLAN_ID" | python3 -c "import sys,json; d=json.load(sys.stdin); print([i['id'] for i in d['items'] if i['approval_status']=='pending'][0])")
|
||||
curl -s -X POST "http://100.108.208.56:8082/api/meals/items/$ITEM2/deny?scope=never_again" | python3 -m json.tool | head -20
|
||||
# Expected: item.approval_status="denied", item.denial_expires_at=null,
|
||||
# promoted_to_permanent=true, scope="never_again"
|
||||
|
||||
# Test 3: list never-suggest entries
|
||||
FAMILY_ID=$(curl -s "http://100.108.208.56:8082/api/meals/$PLAN_ID" | python3 -c "import sys,json; print(json.load(sys.stdin)['family_profile_id'])")
|
||||
curl -s "http://100.108.208.56:8082/api/never-suggest?family_profile_id=$FAMILY_ID" | python3 -m json.tool
|
||||
# Expected: at least 1 row from test 2
|
||||
|
||||
# Test 4: vote page renders 3 buttons (HTML)
|
||||
TOKEN=$(curl -s "http://100.108.208.56:8082/api/meals/vote/$ITEM2" | head -50)
|
||||
# (token is in the page; for direct test, you'd grab it from a real email)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Email smoke
|
||||
|
||||
To preview the new email without sending it through SendGrid, render the template via the planner (orchestrator step_email). On a local dev DB:
|
||||
|
||||
```bash
|
||||
docker compose exec backend python -c "
|
||||
from app.database import SessionLocal
|
||||
from app.services.orchestrator.steps import step_email
|
||||
from app.models import WeeklyRun
|
||||
db = SessionLocal()
|
||||
run = db.query(WeeklyRun).order_by(WeeklyRun.week_start_date.desc()).first()
|
||||
step_email(run, db)
|
||||
# Mail backend defaults to console in dev — check the docker logs
|
||||
"
|
||||
```
|
||||
|
||||
The email now renders 3 direct-action buttons per recipe (Approve / Deny this week / Never again), each a GET link that consumes the token and records the vote. The legacy "Vote on this meal" link is preserved as a "Open vote page (all 3 options)" secondary link below the buttons.
|
||||
|
||||
To exercise the 2-denial auto-promotion via the email:
|
||||
1. Open the vote page for recipe X (the link is in the email).
|
||||
2. Click "Deny this week" → records the soft denial; page shows "Denied this week. Will not re-suggest for 90 days unless denied again."
|
||||
3. Wait for the next Friday email (or manually re-trigger the email for the same plan). Re-open the vote page for a different item with the same recipe.
|
||||
4. Click "Deny this week" again → records the 2nd denial; page shows "Denied this week. You've denied this recipe recently, so it will not be suggested again (permanently blocked)."
|
||||
|
||||
---
|
||||
|
||||
## Things to look for
|
||||
|
||||
- The webui shows 3 buttons (Approve / Deny this week / Never again) on **pending** meal cards. Approved/denied cards show only the badge.
|
||||
- The email shows 3 direct-action links per recipe plus a secondary "open vote page" link.
|
||||
- Clicking "Deny this week" twice on the same recipe (across weeks or via API) auto-promotes the recipe to a permanent block.
|
||||
- The `NeverSuggest` table grows by exactly 1 row per distinct recipe (idempotent).
|
||||
- The planner does not propose denied recipes (hard filter). `rejected_summary` includes a `soft_denied_recipe` diagnostic bucket if a soft-deny was the cause.
|
||||
- The 90-day decay works on read: a row with `denial_expires_at < now()` is invisible to the soft-deny filter; the recipe becomes eligible again.
|
||||
|
||||
---
|
||||
|
||||
## Rollback (if needed)
|
||||
|
||||
The Sprint 8 change is mostly additive. To revert:
|
||||
|
||||
```bash
|
||||
# 1. Revert the code
|
||||
git revert 09c7525..HEAD # or whichever commit set contains Sprint 8
|
||||
|
||||
# 2. Downgrade the migration
|
||||
docker compose exec backend alembic downgrade -1
|
||||
|
||||
# 3. The new columns become NULL again. Existing denied rows
|
||||
# keep denial_expires_at = NULL (the migration default).
|
||||
# The recipes already in NeverSuggest stay there — the user must
|
||||
# manually DELETE FROM never_suggest WHERE ... if they want to
|
||||
# clean up.
|
||||
```
|
||||
|
||||
The pre-Sprint 8 behavior is what the user originally reported as "the rejected meal came back." Only revert if a regression appears that wasn't there before Sprint 8.
|
||||
|
||||
---
|
||||
|
||||
## Verification log
|
||||
|
||||
(Filled in by the operator after deploy + smoke.)
|
||||
|
||||
- [ ] `git pull` on deployment host → success
|
||||
- [ ] `alembic upgrade head` → applied migration 0016
|
||||
- [ ] Backend rebuild → success
|
||||
- [ ] Frontend rebuild → success
|
||||
- [ ] Browser: 3 buttons appear on pending meal cards
|
||||
- [ ] API: `/api/meals/items/{id}/deny` (no scope) returns 200 with `promoted_to_permanent: false`
|
||||
- [ ] API: `/api/meals/items/{id}/deny?scope=never_again` returns 200 with `promoted_to_permanent: true`
|
||||
- [ ] API: 2nd `/deny` (same recipe) auto-promotes with `promoted_to_permanent: true`
|
||||
- [ ] API: `/api/never-suggest?family_profile_id=...` shows the new row
|
||||
- [ ] Email: 3 buttons per recipe, each a one-click action
|
||||
- [ ] No regression in Sprints 1-7
|
||||
|
||||
---
|
||||
|
||||
**Last updated: 2026-06-05** — code complete, awaiting deploy.
|
||||
@@ -77,7 +77,21 @@ The app looks polished on the surface (Tailwind palette, clean cards, working to
|
||||
> - **Backend changes:** `meals.py` and `shopping_list.py` (new query param) + `0015_normalize_pantry_aisles.py` (cast fix).
|
||||
> - **Verification log:** `Review/sprint5-verification.md`. Deploy is a single batch for Sprints 2-5: backup → migrate → rebuild backend + frontend.
|
||||
>
|
||||
> **Sprint 7 status (in progress, approved 2026-06-05; not yet committed):** Outside-the-audit hotfix driven by user report. **Thread 1 of three open follow-ups from the user's 2026-06-05 message.** Thread 2 (cross-week "rejected" semantics) and Thread 3 (§Future backlog) are deferred until S7 is deployed + verified.
|
||||
> **Sprint 8 status (in progress, approved 2026-06-05; not yet committed):** Thread 2 (cross-week "rejected" semantics) and Thread 3 (§Future backlog) — both surfaced in the user's 2026-06-05 follow-up. **User policy decision (2026-06-05):** "Hard filter. If it is denied this week twice, it should be considered denied for good." That collapses Sprint 8 to the **C + Z** model with a server-side 2-denial auto-escalation.
|
||||
> - **T2.1** Migration `0016_denial_decay_and_scope.py` (NEW). Adds `meal_plan_item.denial_expires_at TIMESTAMPTZ NULL` (partial index on non-NULL) and `meal_plan_vote.denial_scope VARCHAR(16) NULL`. No data migration; existing rows keep `denial_expires_at = NULL` (the filter requires `> now()`, so old denied rows are effectively forgotten after 90d).
|
||||
> - **T2.2** Model: `MealPlanItem.denial_expires_at` + `MealPlanVote.denial_scope`.
|
||||
> - **T2.3** Schema: `MealPlanItemResponse.denial_expires_at`, `VoteRequest.denial_scope`, `VoteResponse.denial_scope`.
|
||||
> - **T2.4** Backend helpers in `app/api/meals.py`: `_apply_denial` (single source of truth for the deny path), `_ensure_never_suggest_recipe` (idempotent NeverSuggest insert), `_has_prior_active_soft_denial` (counting query for the 2-denial auto-escalation check). `DENIAL_DECAY_DAYS = 90`.
|
||||
> - **T2.5** `POST /api/meals/items/{id}/deny?scope=this_week|never_again` (default `this_week`). Returns `{message, item, promoted_to_permanent, scope}`. The auto-promotion check runs server-side.
|
||||
> - **T2.6** `POST /api/meals/vote/{id}` extended: `vote: "approve" | "deny" | "never_again"`. Returns `denial_scope` + `promoted_to_permanent` so the email confirmation page can show what was applied.
|
||||
> - **T2.7** Email HTML page (`/api/meals/vote/{id}` GET) renders 3 buttons (Approve / Deny this week / Never again). One-click direct-vote via `?scope=...` for the email's per-button links; consumes the token via `submit_vote` and renders a confirmation page.
|
||||
> - **T2.8** Email template (`step_email`) renders 3 direct-action links per recipe. The legacy single-link "Vote on this meal" is preserved as a secondary "Open vote page (all 3 options)" link.
|
||||
> - **T2.9** Planner: `_load_blocklists` returns 3 sets; `soft_denied_recipes` is hard-filtered (per user decision). `rejected_summary` adds a `soft_denied_recipe` diagnostic bucket.
|
||||
> - **T2.10** Webui: `MealCard` renders 3 buttons (Approve / Deny this week / Never again) for **pending** items. `handleDeny` is scope-aware; toast reflects the server's `promoted_to_permanent` flag. "Never again" is gated by a `window.confirm` to prevent accidental permanent blocks.
|
||||
> - **Verification log:** `Review/sprint8-verification.md`.
|
||||
> - **No new dependencies. Migration is required (`alembic upgrade head` runs 0016). Deploy is `git pull` + migration + `docker compose up -d --build backend frontend`.**
|
||||
>
|
||||
> **Sprint 7 status (commit `09c7525`, awaiting deploy):** Outside-the-audit hotfix driven by user report. **Thread 1 of three open follow-ups from the user's 2026-06-05 message.** Thread 2 (cross-week "rejected" semantics) and Thread 3 (§Future backlog) are deferred until S7 is deployed + verified.
|
||||
> - **T1.1** `runner._current_week_start()` → returns the **upcoming** Monday. Today (Fri 2026-06-05) the function returned Friday 2026-06-05; the user got an email for week-of-2026-06-05, but the webui opened on week-of-2026-06-01. One-line body change in `backend/app/services/orchestrator/runner.py:20-24`. Scheduler cron stays Friday.
|
||||
> - **T1.2** `isoMonday` → `upcomingMonday` in `frontend/src/lib/utils.ts:44-50`. Same logic as T1.1; rename for intent. Add `formatWeekRange(mondayIso)` helper.
|
||||
> - **T1.3** New `frontend/src/components/WeekRangeNav.tsx`. Renders the user-requested `[<] Jun 8 — Jun 14 [>]` pattern with clickable chevrons and a clickable range label (jumps to the upcoming week). Replaces the inline Sprint 5 segmented control on Dashboard and ShoppingList. Includes a `This week` chip when off the upcoming week. Keyboard-accessible.
|
||||
|
||||
Reference in New Issue
Block a user