diff --git a/HANDOFF.md b/HANDOFF.md index f8a4d0f..d419705 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -5,9 +5,11 @@ first; this file only records progress and session-specific notes. ## Where we are in the plan -**Milestone M1 (Manual calorie tracker) is COMPLETE.** Tickets 001–005 all -implemented, QA-verified, and committed. The app is usable end-to-end for -manual food entry and daily logging. +**Milestones M1 (Manual calorie tracker) and M2 (Barcode scanning & OFF +integration) are COMPLETE.** Tickets 001–006 all implemented, QA-verified, +and committed. The app is usable end-to-end for manual food entry, daily +logging, barcode scanning (manual-barcode path verified; phone camera path +pending user testing), and OpenFoodFacts lookup/search. | Ticket | Status | Commit | |--------|--------|--------| @@ -16,12 +18,12 @@ manual food entry and daily logging. | 003 Daily log write path | ✅ done, QA passed | `87d7eca` | | 004 Day summary endpoint | ✅ done, QA passed | `5372e8c` | | 005 Frontend daily view | ✅ done, QA passed | `b69661c` | -| 006 OFF + barcode scan/search flows | ⬜ **next** | — | -| 007 Meals (from-log, unpack, recursion) | ⬜ pending | — | +| 006 OFF + barcode scan/search flows | ✅ done, QA passed (backend `32461b7`, frontend below) | `32461b7` + frontend | +| 007 Meals (from-log, unpack, recursion) | ⬜ **next** | — | | 008 Food library view + restore | ⬜ pending | — | -Test counts at HEAD: backend **104 passed** (`cd backend && uv run pytest`), -frontend vitest + `npm run build` green. +Test counts at HEAD: backend **152 passed** (`cd backend && uv run pytest`), +frontend **18 vitest passed** + `npm run build` green. ## What was done this session @@ -42,40 +44,80 @@ frontend vitest + `npm run build` green. layout. All HTTP via `lib/api.js`; shared state in `stores.svelte.js`; Svelte 5 runes only. -## Process lessons (important for the next orchestrator) +## Process notes (session 2) -1. **Implementer agents can falsely report success without writing code.** - This happened twice (TICKET-004 backend, first TICKET-005 frontend attempt). - Mitigations that worked: - - Every implementer task must require `git status --short` evidence and a - live smoke test (curl the endpoint / build + serve) in its report. - - QA must be explicitly told not to trust the implementer's report and to - verify the feature exists (e.g. check `/openapi.json` routes) before - testing. -2. QA (playwright) genuinely catches real bugs — it found broken date-nav - buttons and a stale-summary bug in TICKET-005; both were fixed and - re-verified before commit. -3. Backend servers go stale between tickets (old uvicorn missing new routes). - QA handles restarts, but expect it. +- Baked the retrospective's structural fixes into the agent definitions + (chore `f8048da`): be/fe-implementer now MUST paste `git status`, test + output, and a live smoke test; qa is adversarial (distrust self-reports, + confirm features via `/openapi.json`), restarts both servers + resets the + dev DB before testing, and writes expected numbers into scenarios. This + eliminated the false-success-report failure mode on 006. +- `.playwright-cli/` + `*.png` are now gitignored; QA writes artifacts to + `/tmp`. +- The scout agent earned its keep on 006: primed exact seams for both + stacks, saving re-derivation. Its one slip (recent-foods ordering by + `created_at` vs `daily_log` appearance) was caught and corrected in the + implementer prompt. +- Split 006 into backend → QA(curl) → commit, then frontend → QA(playwright) + → commit, with an orchestrator diff review before each QA run. Two commits + for one ticket (across stacks) gave clean checkpoints. +- Bug-fix loop on 006 backend (OFF search 500 on upstream 503) was cheap and + effective — one focused implementer call + verify. -## Where to pick up: TICKET-006 (Milestone M2) +## Where to pick up: TICKET-007 (Milestone M3 — Meals) -OFF normalization + barcode scan & search flows. Depends on 005 (done). -Read the TICKET-006 section of `IMPLEMENTATION_PLAN.md` — key points: +Meals: `meal_components` table, `POST /api/meals/from-log`, `POST +/api/meals/{meal_id}/unpack`, `PUT /api/meals/{meal_id}/components`, and +real derived meal nutrition (recursive component summation + cycle +detection in `services/nutrition.py`). Read the TICKET-007 section of +`IMPLEMENTATION_PLAN.md` — key points: -- Backend: OFF → foods normalization in exactly one module (kcal/kJ mapping, - User-Agent, timeouts); `GET /api/off/product/{barcode}`, `GET /api/off/search`, - `POST /api/off/refresh/{food_id}`; restore-on-rescan rule (§3.1); implement - `GET /api/foods/recent`; httpx mocked at the boundary in tests. -- Frontend: `BarcodeScanner.svelte` + `lib/scanner.js` (native - `BarcodeDetector` with lazy zxing-wasm fallback, ~3–5 fps decode loop, - camera teardown on destroy); scan flow per §4.1 with manual barcode - fallback; OFF fallback in search per §4.2; recent foods surfaced in UI. -- **Camera testing caveat (from the user):** a webcam exists but real scanner - verification (esp. the §8.4 phone checklist — Android Chrome, iOS Safari, - EAN-13/UPC-A) needs the user manually over Caddy HTTPS. Don't block the - ticket on camera QA: verify the manual-barcode fallback and OFF search - flows via playwright, and mark the phone checklist as pending user testing. +- **This is the highest-complexity ticket.** Recursion, cycle detection, + and transactional rollback are the spec's named testing priorities + (§8.4). Build the service layer first with direct unit tests, then wire + routers. Run implementer and QA as **separate** calls (not a chain) with + an orchestrator diff review in between (retrospective lesson #11). +- Backend: `POST /api/meals/from-log` and `POST /api/meals/{meal_id}/unpack` + are one-transaction-each (commit once or roll back entirely — §8.1 rule 6). + `PUT /api/meals/{meal_id}/components` replaces the component list wholesale, + cycle-checked. Meal nutrition = recursive component summation with a + visited-set, both for nutrition reads and cycle checks on write. +- Remove the ticket-004 TODO (meals contributing 0 to summary). Summary + (ticket 004), `GET /api/foods/{id}`, and `GET /api/log` responses must now + show real derived meal nutrition; log responses include meal components + nested for the collapsible UI (§3.3). +- Unpack quantity scaling (§3.2): each component's quantity × the original + meal entry's scaling factor (1.5× meal → 1.5× each component). Pin this + with a dedicated test. +- Cycle attempts (meal containing itself, directly or transitively) → 422. +- Frontend: "Save as Meal" (multi-select today's entries → name → replace + with meal entry), collapsible meal rows (collapsed = name + total kcal, + expanded = components), "Unpack" action, meal component editing from the + food editor for `is_meal` foods (§4.7). + +## TICKET-006 notes (for reference / loose ends) + +- Backend OFF normalizer is `backend/services/off.py` (the ONE module, + §8.1 rule 10): kcal/kJ fallback, not-found rule, graceful degradation on + upstream 503/timeout (returns `None`/`[]`, never 500). `GET /api/off/product`, + `/api/off/search`, `POST /api/off/refresh/{food_id}` (404 unknown id, 400 + no-barcode). Restore-on-rescan in `services/foods.py` `create_food()`. + `GET /api/foods/recent` orders by `daily_log.created_at` (most recent log + ACTION, deliberately NOT by the log `date` field — see docstring; QA once + read this as a bug, it's a tested, deliberate choice). +- Frontend scan flow lives in `App.svelte` as a phase state machine + (`null`/`localFound`/`offFound`/`notFound`/`error`). `lib/scanner.js` is + the camera+decode loop (native BarcodeDetector + lazy zxing-wasm, + ~4fps, teardown in `stop()`). `BarcodeScanner.svelte` stops on first + detect + on destroy (§8.2 rule 6). +- **§8.4 phone checklist PENDING USER TESTING** (native BarcodeDetector on + Android Chrome, zxing-wasm on iOS Safari, EAN-13/UPC-A decode). Requires + Caddy HTTPS. The manual-barcode fallback + OFF search flows are + playwright-verified; do not regress them. +- **Dev-only Vite proxy caching** observed by QA: empty OFF search + responses were cached within a Vite dev session; a fresh Vite restart + cleared it. Not a code defect (production build unaffected). If it + recurs, check the vite proxy config (`changeOrigin`, cache headers). ## Loose ends / chores diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte index 2fabbbd..ca11f09 100644 --- a/frontend/src/App.svelte +++ b/frontend/src/App.svelte @@ -1,15 +1,20 @@ -

BarcodeScanner (placeholder)

+
+ {#if error} + + {/if} + + {#if loading && !cameraUnavailable} +

Starting camera…

+ {/if} + + +
+ + {#if loading} +

Starting camera…

+ {/if} +
+ + +
+

+ {#if cameraUnavailable} + Enter a barcode number manually: + {:else} + Or type a barcode: + {/if} +

+
+ + +
+
+
+ + diff --git a/frontend/src/components/Dashboard.svelte b/frontend/src/components/Dashboard.svelte index 690f4a3..e516a33 100644 --- a/frontend/src/components/Dashboard.svelte +++ b/frontend/src/components/Dashboard.svelte @@ -1,9 +1,12 @@
- + -

Create food

+ {#if justSaved} + +

"{justSaved.food.name}" saved

-
- +
+

{justSaved.food.brand}

- - -
- Unit type -
- + {#if logError}{/if} -
- Macros (optional, per {unitType === 'weight' ? '100g' : 'item'}) -
- - - +
+ +
-
+
+ + {:else} + +

{isPrefilled ? 'Confirm & edit food' : 'Create food'}

+ + + + + + + + + {#if isPrefilled} +

Source: {source}

+ {/if} - {#if unitType === 'weight'}
- Serving info (optional) -
- {/if} - {#if error}{/if} + -
- - -
-
+
+ Macros (optional, per {unitType === 'weight' ? '100g' : 'item'}) +
+ + + +
+
+ + {#if unitType === 'weight'} +
+ Serving info (optional) + + +
+ {/if} + + {#if error}{/if} + +
+ + +
+ + {/if}
diff --git a/frontend/src/components/FoodSearch.svelte b/frontend/src/components/FoodSearch.svelte index 3801e18..a4f7111 100644 --- a/frontend/src/components/FoodSearch.svelte +++ b/frontend/src/components/FoodSearch.svelte @@ -1,17 +1,24 @@