## Outcome Read-only mapping completed at base `039bbeaca97770af9f650754efd80d1977fd49d4`. Repository remained clean; no files were created or modified. ## Existing nutrition-day flow - Route allowlisting and dispatch: - `scripts/health/dashboard_v5/read_api.py:100-110` - Dynamic route allowlist: `read_api.py:2644-2651` - `/api/v1/nutrition/day/YYYY-MM-DD` dispatch, no query parameters, future dates rejected: `read_api.py:2739-2745` - Detailed provider: - `_nutrition_day_detail`: `read_api.py:1340-1475` - Bounded to 200 products: `read_api.py:65`, `1413-1422` - Reads daily summary, nutrient rows, meal summaries, products and histamine mappings. - Central day endpoint: - Abbreviated nutrition provider: `read_api.py:1231-1259` - Included in `/api/v1/day/...`: `read_api.py:1517-1606` - Day UI: - Day section markup: `scripts/health/dashboard_v5/render.py:88-104` - Nutrition is currently only a generic `
`: `render.py:96` - Day controller renders only item count/completeness from the abbreviated central payload: `scripts/health/assets/health-assets/dashboard-v5-day-controller.js:109-134` - It fetches only `/api/v1/day/...`: `dashboard-v5-day-controller.js:138-153` - Request-version stale protection exists: `dashboard-v5-day-controller.js:16`, `97`, `140`, `151-152` - Each request has a ten-second timeout controller, but newer day selections do **not** abort older requests: `dashboard-v5-day-controller.js:51-63`. ## Current `/api/v1/nutrition/day/{date}` payload For a documented day: ```text { version, date, timezone, status, definition: { id, version, components[], coverage_semantics, insufficient_inputs, score_scale, complete_rule }, coverage: { status, item_count, unknown_items, mapping_coverage, insufficient_inputs }, macros: { kcal, protein_g, carb_g, fat_g }, nutrients: { : { label, value, unit } }, histamine: { score, max_score, unknown_items, definition_version }, meals: [{ meal, label, item_count, macros: { kcal, protein_g, carb_g, fat_g }, histamine: { score, status } }], items: [{ meal, name, brand, amount, amount_unit, macros: { kcal, protein_g, carb_g, fat_g }, histamine: { canonical_food, score, status, confidence } }] } ``` Construction is at `read_api.py:1447-1475`; products are built at `1428-1446`, meals at `1391-1411`, and nutrients at `1353-1382`. For no data, the shape collapses to only: ```text { version, date, timezone, status } ``` See `read_api.py:1342-1351`. This shape asymmetry must be handled or normalized before rendering. The central `/api/v1/day/{date}` payload contains only: ```text nutrition: { status, date, kcal, protein_g, carb_g, fat_g, item_count, histamine_score, histamine_status, unknown_items, complete } ``` See `read_api.py:1247-1259`. ## Safe allowlist / later nutrient inventory Keys and units only, with no observations or product identities: | Key | Unit | |---|---| | `energy.energy` | `kcal` | | `nutrient.protein` | `g` | | `nutrient.carb` | `g` | | `nutrient.fat` | `g` | | `nutrient.fiber` | `g` | | `nutrient.sugar` | `g` | | `nutrient.saturated` | `g` | | `nutrient.salt` | `g` | | `nutrient.sodium` | `mg` | Canonical API allowlist: `read_api.py:74-84`. The importer uses the same key/unit inventory at `scripts/health/yazio_nutrition_sync.py:47-64`. Additional safe enums: - Meals: `breakfast`, `lunch`, `dinner`, `snack`, `unassigned` — `read_api.py:85-91` - Amount units: `g`, `ml`, `piece`, `portion`; all others become `unknown` — `read_api.py:1432-1433` - Histamine score: `0..3` or unknown - Confidence: `low`, `medium`, `high` — `read_api.py:1442-1444` - Mapping decisions/methods are server-returned at `read_api.py:1509-1513`. Do **not** release source IDs, raw JSON, hashes, paths, URLs, provenance notes, personal-tolerance data, or database row IDs. Existing tests enforce much of this at `tests/test_dashboard_v5_sprint6g_nutrition.py:80-127`. ## Mapping UI and queue identity - Bundle mapping provider: `scripts/health/dashboard_v5/data_provider.py:276-292` - Queue API: `read_api.py:1478-1514` - Opaque identity function: - Canonical implementation: `scripts/health/dashboard_v5/contracts.py:18-21` - Duplicated worker implementation: `scripts/health/health_dashboard_action_worker.py:277-279` - Mapping UI/forms: `scripts/health/assets/health-assets/dashboard-v5.js:510-657` - Form binds opaque `queue_key` and exact alias: `dashboard-v5.js:539-555` - Queue submission validation: `scripts/health/health_dashboard_server.py:510-592` - Worker verifies: - exactly one open queue hash, - exact alias, - no normalized-name collision, - exact matching products: `health_dashboard_action_worker.py:631-664` - Collision regression: `tests/test_dashboard_v5_sprint6g_nutrition.py:443-498`. There is currently no queue identity in nutrition-day product items, so a safe mapping deep-link cannot be produced without backend enrichment. The client must **not** derive a queue key from a displayed product string. ## Exact implementation plan 1. **Enrich the nutrition-day contract safely** - `scripts/health/dashboard_v5/read_api.py:1340-1475` - Load open queue rows once and match only a unique exact `example_name`. - Add a bounded per-product mapping object, e.g.: ```text mapping: { status, queue_key } ``` where `queue_key` is present only for a unique open queue match. - Keep canonical mapping facts under `histamine`; do not expose normalized names or source IDs. - Return the same full top-level shape with empty arrays/maps for `supported_no_data`. - Preserve `MAX_NUTRITION_ITEMS` and text sanitization. 2. **Fix meal normalization while touching the provider** - `read_api.py:1391-1411` - Normalize every unknown source meal to `unassigned` before sorting. - Current code emits `"unknown"` at `1393`, but the sort order at `1411` has no `"unknown"` entry and can raise `ValueError`. 3. **Fetch detail from the central day controller** - `scripts/health/assets/health-assets/dashboard-v5-day-controller.js:16-21` - Add one active nutrition-detail `AbortController`. - `dashboard-v5-day-controller.js:51-63` - Extend `request` to accept an external signal while retaining timeout behavior and distinguishing superseded aborts from timeouts. - `dashboard-v5-day-controller.js:138-153` - Abort the prior detail request when opening another date. - Fetch `/api/v1/nutrition/day/${date}` for non-future documented nutrition days. - Retain `dayRequestVersion` checks both before and after rendering so a late response cannot overwrite the selected day. - Do not call the nutrition endpoint for future days: the central day endpoint allows limited future dates, but nutrition detail rejects all future dates (`read_api.py:2733-2745`). 4. **Render full day nutrition** - `dashboard-v5-day-controller.js:109-134` - Replace the one-line summary at `121-127` with DOM-only rendering for: - day macros, - coverage/completeness, - histamine status/load, - meal cards, - product rows with amount/macros/mapping status, - allowlisted nutrients. - Preserve null as “not documented”; never coerce missing values to zero. - Render all user-derived text through `textContent`. - `scripts/health/dashboard_v5/render.py:96` - Add stable subcontainers/status targets inside the day nutrition section. - Responsive styles belong near: - day grid: `scripts/health/assets/health-assets/dashboard-v5.css:311-323` - nutrition/mapping controls: `dashboard-v5.css:465-469`. 5. **Implement opaque mapping deep-links** - `scripts/health/assets/health-assets/dashboard-v5.js:378-390` - Add a nutrition mapping navigation helper that emits only: ```text ?view=nutrition&mapping=<32 lowercase hex> ``` - `dashboard-v5.js:521-657` - Put `data-mapping-queue-key` on each mapping card. - On initial load and `popstate` (`dashboard-v5.js:1322-1327`), validate exact query shape, open only the matching card, scroll it into view and focus its summary/heading. - Expose a narrow callback for the day controller rather than duplicating routing logic. - Never put product name, alias, canonical food, notes, or normalized identity into the URL. 6. **Contract/provider tests** - Extend `tests/test_dashboard_v5_sprint6g_nutrition.py:61-131` with: - exact documented and no-data shapes, - unique unmapped item receives only an opaque queue key, - mapped item has no queue deep-link, - ambiguous identity fails closed, - forbidden metadata fields remain absent, - unknown meal becomes `unassigned`, - future date remains rejected. 7. **Browser tests** - Extend `tests/browser/dashboard_v5_calendar_day_6d.spec.js:68-167` with delayed, out-of-order nutrition responses: - first request is aborted after selecting a second day, - stale detail never overwrites the second day, - timeout/retry remains accessible. - Extend `tests/browser/dashboard_v5_sprint6g_echarts.spec.js:72-139` for full meals/products/macros/mapping/histamine rendering and chart-to-day navigation. - Extend mapping coverage around `dashboard_v5_sprint6g_echarts.spec.js:151-190` for deep-link focus, reload, back/forward, invalid/duplicate mapping parameters, and no names in URL. 8. **Fixtures** - Existing synthetic fixture already provides complete and incomplete days, meals, product macros, allowlisted/unallowlisted nutrients, mapped/unmapped items, provenance and an open queue: `tests/fixtures/dashboard_v5_fixture.py:241-361`. - Reuse it; add only an unknown-meal row and, if needed, a second normalized collision row. No real records are needed. ## Key risks - No formal exact-shape validator currently protects the standalone nutrition-day response; it is only hand-built. - No-data and documented response shapes differ. - Older nutrition-day requests are not abortable today. - The separate nutrition tab loader also lacks abort/stale protection: `dashboard-v5.js:676-693`. - Queue identity is absent from day products; client-side name hashing would bypass the worker’s collision safeguards. - `"unknown"` meal summaries can currently crash sorting. - Nutrient fallback runs only when the nutrient table is entirely absent, not when it exists but has no rows for the selected day: `read_api.py:1355-1379`. - Mapping UI uses embedded bundle rows rather than `/api/v1/nutrition/mapping-queue`; the two contracts can drift. - `nutrition_queue_key` is duplicated between contracts and worker, creating drift risk. - Product ordering is currently lexical by meal (`read_api.py:1415-1419`), not the displayed meal order. - Macronutrient units are encoded in field names rather than returned explicitly; later expansion should remain key/unit allowlist-driven.