## 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.