TICKET-006 (frontend): barcode scanner, OFF scan/search flows, recent foods

- lib/scanner.js: native BarcodeDetector + lazy zxing-wasm fallback, ~4fps
  decode loop, camera teardown in stop() (§8.2 rule 6)
- BarcodeScanner.svelte: getUserMedia camera, permission-denied → message +
  manual barcode text input fallback, stop on first detect + on destroy
- App.svelte: §4.1 scan-flow state machine (localFound/offFound/notFound/
  error) — local-by-barcode first, OFF lookup → pre-fill FoodEditor → save
  → log, Refresh-from-OFF button, not-found → manual/search options
- FoodEditor.svelte: optional food prop pre-fills from OFF (read-only
  barcode, source badge), post-save log flow
- FoodSearch.svelte: OFF fallback (§4.2) when local results empty
- Dashboard.svelte: recent foods quick-log chips
- api.js: searchFoodsByBarcode, offRefresh, updateFood, recentFoods
- scanner.test.js: 2 unit tests for pure helpers
- §8.4 phone checklist deferred to user testing over Caddy HTTPS;
  manual-barcode + OFF search flows playwright-verified
- HANDOFF.md updated: M1+M2 complete, 007 next
This commit is contained in:
Craig
2026-07-26 16:19:26 +01:00
parent 32461b7405
commit 4dd44b08d0
9 changed files with 1300 additions and 159 deletions
+79 -37
View File
@@ -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 001005 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 001006 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, ~35 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