Success. Updated the following files:
M scripts/health/assets/health-assets/dashboard-v5.js

@@ -414,3 +414,3 @@
       const card = node("article", "v5-card");
-      card.append(node("h3", "v5-label", "Letzter vollständig klassifizierter Histamin-Score"));
+      card.append(node("h3", "v5-label", "Letzter vollständig klassifizierter Histamin-Index"));
       card.append(node("div", "v5-kpi", point ? formatValue(point.value) : "unbekannt"));
@@ -431,3 +431,10 @@
         form.action = "/health-actions/nutrition-mapping";
-        [["csrf_token", q("#view-nutrition")?.dataset.mappingCsrf || ""], ["return_to", "v5"], ["queue_key", item.queue_key || ""], ["canonical_food", item.suggested_canonical_food || ""], ["sighi_score", item.suggested_score ?? "unknown"], ["traffic_light", item.suggested_traffic_light || "unknown"], ["mapping_scope", "canonical"], ["personal_tolerance", "unknown"]].forEach(([name, value]) => {
+        [
+          ["csrf_token", q("#view-nutrition")?.dataset.mappingCsrf || ""],
+          ["return_to", "v5"],
+          ["queue_key", item.queue_key || ""],
+          ["alias", item.example_name || item.name || ""],
+          ["source_label", "SIGHi"],
+          ["source_version", "unknown"],
+        ].forEach(([name, value]) => {
           const input = node("input");
@@ -438,3 +445,67 @@
         });
-        const submit = node("button", "", "Zuordnung vormerken");
+        const decision = node("select");
+        decision.name = "decision";
+        [["assign", "Zuordnen"], ["composite", "Zutatenprüfung"], ["ignore", "Ignorieren"]].forEach(([value, label]) => {
+          const option = node("option", "", label);
+          option.value = value;
+          decision.append(option);
+        });
+        const canonical = node("input");
+        canonical.name = "canonical_food";
+        canonical.maxLength = 120;
+        canonical.value = item.suggested_canonical_food || "";
+        const score = node("select");
+        score.name = "sighi_score";
+        ["unknown", "0", "1", "2", "3"].forEach((value) => {
+          const option = node("option", "", value === "unknown" ? "SIGHi unbekannt" : `SIGHi ${value}`);
+          option.value = value;
+          if (String(item.suggested_score ?? "unknown") === value) option.selected = true;
+          score.append(option);
+        });
+        const confidence = node("select");
+        confidence.name = "confidence";
+        [["low", "niedrig"], ["medium", "mittel"], ["high", "hoch"]].forEach(([value, label]) => {
+          const option = node("option", "", label);
+          option.value = value;
+          if (value === "medium") option.selected = true;
+          confidence.append(option);
+        });
+        const method = node("select");
+        method.name = "mapping_method";
+        [["sighi_reference", "SIGHi-Referenz"], ["ingredient_label", "Zutatenliste"], ["manual_review", "manuelle Prüfung"], ["local_alias", "lokaler Alias"]].forEach(([value, label]) => {
+          const option = node("option", "", label);
+          option.value = value;
+          method.append(option);
+        });
+        const note = node("input");
+        note.name = "note";
+        note.maxLength = 300;
+        note.value = item.reason || "";
+        const reviewRequired = node("input");
+        reviewRequired.type = "hidden";
+        reviewRequired.name = "ingredient_review_required";
+        reviewRequired.value = "false";
+        decision.addEventListener("change", () => {
+          const composite = decision.value === "composite";
+          const ignore = decision.value === "ignore";
+          reviewRequired.value = composite ? "true" : "false";
+          score.value = composite || ignore ? "unknown" : score.value;
+          canonical.value = ignore ? "" : canonical.value;
+          canonical.disabled = ignore;
+          score.disabled = composite || ignore;
+        });
+        [
+          ["Entscheidung", decision],
+          ["Kanonisches Lebensmittel", canonical],
+          ["SIGHi", score],
+          ["Vertrauen", confidence],
+          ["Methode", method],
+          ["Notiz", note],
+        ].forEach(([labelText, control]) => {
+          const label = node("label", "mapping-field", labelText);
+          label.append(control);
+          form.append(label);
+        });
+        form.append(reviewRequired);
+        const submit = node("button", "", "Aktion vormerken");
         submit.type = "submit";


Output:
Exit code: 0
Wall time: 0.1 seconds
Output:
Success. Updated the following files:
M scripts/health/assets/health-assets/dashboard-v5.js


The workspace now renders real controls for the mapping decision instead of a single hidden “map it” form. I noticed disabled form controls would not submit, so I’m keeping the controls submitted and enforcing the composite/ignore invariants in both browser payload and server validation.


Input:
*** Begin Patch
*** Update File: scripts/health/assets/health-assets/dashboard-v5.js
@@
           reviewRequired.value = composite ? "true" : "false";
           score.value = composite || ignore ? "unknown" : score.value;
           canonical.value = ignore ? "" : canonical.value;
-          canonical.disabled = ignore;
-          score.disabled = composite || ignore;
+          canonical.readOnly = ignore;
         });
*** End Patch


@@ -493,4 +493,3 @@
           canonical.value = ignore ? "" : canonical.value;
-          canonical.disabled = ignore;
-          score.disabled = composite || ignore;
+          canonical.readOnly = ignore;
         });


Output:
Exit code: 0
Wall time: 0.1 seconds
Output:
Success. Updated the following files:
M scripts/health/assets/health-assets/dashboard-v5.js


I’m updating schema defaults and provenance columns so fresh fixtures and bootstrap-created DBs match the new neutral contract. Existing raw nutrition item tables remain untouched; recomputation only writes summary/mapping tables.


Input:
*** Begin Patch
*** Update File: database/schema.sql
@@
-    histamine_score REAL DEFAULT 0,
+    histamine_score REAL,
@@
-    histamine_score REAL DEFAULT 0,
+    histamine_score REAL,
@@
-    traffic_light TEXT NOT NULL,
+    traffic_light TEXT NOT NULL DEFAULT 'unknown',
@@
-    personal_status TEXT NOT NULL DEFAULT 'unknown' CHECK(personal_status IN ('safe','problematic','unclear','unknown')),
+    personal_status TEXT NOT NULL DEFAULT 'unknown',
@@
     action_hash TEXT NOT NULL,
     queue_key TEXT NOT NULL,
+    decision TEXT,
+    alias TEXT,
+    canonical_food TEXT,
+    sighi_score INTEGER,
+    confidence TEXT,
+    mapping_method TEXT,
+    source_label TEXT,
+    source_version TEXT,
+    note TEXT,
+    ingredient_review_required INTEGER,
+    personal_tolerance TEXT,
+    definition_version TEXT,
     applied_at TEXT DEFAULT CURRENT_TIMESTAMP
 );
@@
     action_id TEXT NOT NULL,
     normalized_name TEXT NOT NULL,
+    decision TEXT,
+    alias TEXT,
     canonical_food TEXT,
     sighi_score INTEGER,
-    traffic_light TEXT NOT NULL,
-    mapping_scope TEXT NOT NULL,
+    traffic_light TEXT,
+    mapping_scope TEXT,
+    confidence TEXT,
+    mapping_method TEXT,
+    source_label TEXT,
+    source_version TEXT,
+    note TEXT,
+    ingredient_review_required INTEGER,
+    personal_tolerance TEXT,
+    definition_version TEXT,
*** End Patch


Output:
apply_patch verification failed: Failed to find expected lines in /home/agent/.hermes/repos/HealthManager/database/schema.sql:
    personal_status TEXT NOT NULL DEFAULT 'unknown' CHECK(personal_status IN ('safe','problematic','unclear','unknown')),


One schema hunk did not match because the local file’s formatting differs around the tolerance table. I’m reading that block and applying a narrower patch.


Output:
Chunk ID: 025629
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1088
Output:
CREATE TABLE nutrition_daily_summary_v2 (
    datum TEXT PRIMARY KEY,
    kcal REAL DEFAULT 0,
    protein_g REAL DEFAULT 0,
    carb_g REAL DEFAULT 0,
    fat_g REAL DEFAULT 0,
    item_count INTEGER DEFAULT 0,
    nutrient_json TEXT,
    histamine_score REAL DEFAULT 0,
    histamine_max INTEGER DEFAULT 0,
    histamine_unknown_count INTEGER DEFAULT 0,
    histamine_label TEXT DEFAULT 'unknown',
    updated_at TEXT DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE nutrition_food_insights (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    canonical_food TEXT NOT NULL,
    insight_type TEXT NOT NULL CHECK(insight_type IN ('trigger_candidate','safe_candidate','insufficient_data')),
    days_seen INTEGER DEFAULT 0,
    avg_same_day_symptom REAL,
    avg_next_day_symptom REAL,
    avg_followup_symptom REAL,
    histamine_avg REAL,
    confidence TEXT DEFAULT 'low',
    notes TEXT,
    computed_at TEXT DEFAULT CURRENT_TIMESTAMP,
    UNIQUE(canonical_food, insight_type)
);

CREATE TABLE nutrition_histamine_scores (
    item_id INTEGER PRIMARY KEY,
    canonical_food TEXT,
    sighi_score INTEGER,
    traffic_light TEXT NOT NULL,
    tags TEXT,
    confidence TEXT DEFAULT 'low',
    reason TEXT,
    scored_at TEXT DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (item_id) REFERENCES nutrition_items(id) ON DELETE CASCADE
);

CREATE TABLE nutrition_item_nutrients (
    item_id INTEGER NOT NULL,
    nutrient_key TEXT NOT NULL,
    value REAL,
    unit TEXT,
    raw_value TEXT,
    PRIMARY KEY (item_id, nutrient_key),
    FOREIGN KEY (item_id) REFERENCES nutrition_items(id) ON DELETE CASCADE
);

CREATE TABLE nutrition_items (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    source TEXT NOT NULL DEFAULT 'yazio_api',
    source_item_id TEXT,
    source_product_id TEXT,
    datum TEXT NOT NULL,
    meal TEXT NOT NULL,
    name TEXT NOT NULL,
    brand TEXT,
    amount REAL,
    amount_unit TEXT DEFAULT 'g',
    serving TEXT,
    serving_quantity REAL,
    kcal REAL,
    protein_g REAL,
    carb_g REAL,
    fat_g REAL,
    raw_item_json TEXT,
    raw_product_json TEXT,
    item_hash TEXT NOT NULL UNIQUE,
    imported_at TEXT DEFAULT CURRENT_TIMESTAMP,
    updated_at TEXT DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE nutrition_meal_summary (
    datum TEXT NOT NULL,
    meal TEXT NOT NULL,
    kcal REAL DEFAULT 0,
    protein_g REAL DEFAULT 0,
    carb_g REAL DEFAULT 0,
    fat_g REAL DEFAULT 0,
    item_count INTEGER DEFAULT 0,
    nutrient_json TEXT,
    histamine_score REAL DEFAULT 0,
    histamine_label TEXT DEFAULT 'unknown',
    updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (datum, meal)
);

CREATE TABLE nutrition_review_queue (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    normalized_name TEXT NOT NULL UNIQUE,
    example_name TEXT NOT NULL,
    occurrence_count INTEGER DEFAULT 0,
    first_seen TEXT,
    last_seen TEXT,
    suggested_canonical_food TEXT,
    suggested_score INTEGER,
    reason TEXT,
    status TEXT DEFAULT 'open' CHECK(status IN ('open','mapped','ignored')),
    updated_at TEXT DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE nutrition_treatment_phase_summary (
        phase TEXT PRIMARY KEY,
        start_date TEXT,
        end_date TEXT,
        nutrition_days INTEGER DEFAULT 0,
        avg_histamine_score REAL,
        avg_symptom_score REAL,
        avg_kcal REAL,
        avg_protein_g REAL,
        notes TEXT,
        computed_at TEXT DEFAULT CURRENT_TIMESTAMP
    );

CREATE TABLE personal_food_tolerance (
    canonical_food TEXT PRIMARY KEY,
    personal_status TEXT NOT NULL DEFAULT 'unknown' CHECK(personal_status IN ('safe','problematic','unclear','unknown')),
    evidence_level TEXT DEFAULT 'low' CHECK(evidence_level IN ('low','medium','high')),
    notes TEXT,
    updated_at TEXT DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE nutrition_mapping_action_log (
    action_id TEXT PRIMARY KEY,
    action_hash TEXT NOT NULL,
    queue_key TEXT NOT NULL,
    applied_at TEXT DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE nutrition_mapping_provenance (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    action_id TEXT NOT NULL,
    normalized_name TEXT NOT NULL,
    canonical_food TEXT,
    sighi_score INTEGER,
    traffic_light TEXT NOT NULL,
    mapping_scope TEXT NOT NULL,
    affected_day TEXT NOT NULL,
    applied_at TEXT DEFAULT CURRENT_TIMESTAMP,
    UNIQUE(action_id, affected_day)
);


Output:
Chunk ID: b255c7
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 213
Output:
        notes TEXT,
        computed_at TEXT DEFAULT CURRENT_TIMESTAMP
    );

CREATE TABLE personal_food_tolerance (
    canonical_food TEXT PRIMARY KEY,
    personal_status TEXT NOT NULL DEFAULT 'unknown' CHECK(personal_status IN ('safe','problematic','unclear','unknown')),
    evidence_level TEXT DEFAULT 'low' CHECK(evidence_level IN ('low','medium','high')),
    notes TEXT,
    updated_at TEXT DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE nutrition_mapping_action_log (
    action_id TEXT PRIMARY KEY,
    action_hash TEXT NOT NULL,
    queue_key TEXT NOT NULL,
    applied_at TEXT DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE nutrition_mapping_provenance (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    action_id TEXT NOT NULL,
    normalized_name TEXT NOT NULL,
    canonical_food TEXT,
    sighi_score INTEGER,
    traffic_light TEXT NOT NULL,


Input:
*** Begin Patch
*** Update File: database/schema.sql
@@
-    histamine_score REAL DEFAULT 0,
+    histamine_score REAL,
@@
-    traffic_light TEXT NOT NULL,
+    traffic_light TEXT NOT NULL DEFAULT 'unknown',
@@
-    histamine_score REAL DEFAULT 0,
+    histamine_score REAL,
@@
-    personal_status TEXT NOT NULL DEFAULT 'unknown' CHECK(personal_status IN ('safe','problematic','unclear','unknown')),
+    personal_status TEXT NOT NULL DEFAULT 'unknown',
@@
     action_hash TEXT NOT NULL,
     queue_key TEXT NOT NULL,
+    decision TEXT,
+    alias TEXT,
+    canonical_food TEXT,
+    sighi_score INTEGER,
+    confidence TEXT,
+    mapping_method TEXT,
+    source_label TEXT,
+    source_version TEXT,
+    note TEXT,
+    ingredient_review_required INTEGER,
+    personal_tolerance TEXT,
+    definition_version TEXT,
     applied_at TEXT DEFAULT CURRENT_TIMESTAMP
 );
@@
     action_id TEXT NOT NULL,
     normalized_name TEXT NOT NULL,
+    decision TEXT,
+    alias TEXT,
     canonical_food TEXT,
     sighi_score INTEGER,
-    traffic_light TEXT NOT NULL,
-    mapping_scope TEXT NOT NULL,
+    traffic_light TEXT,
+    mapping_scope TEXT,
+    confidence TEXT,
+    mapping_method TEXT,
+    source_label TEXT,
+    source_version TEXT,
+    note TEXT,
+    ingredient_review_required INTEGER,
+    personal_tolerance TEXT,
+    definition_version TEXT,
*** End Patch


Success. Updated the following files:
M database/schema.sql

@@ -313,3 +313,3 @@
     nutrient_json TEXT,
-    histamine_score REAL DEFAULT 0,
+    histamine_score REAL,
     histamine_max INTEGER DEFAULT 0,
@@ -339,3 +339,3 @@
     sighi_score INTEGER,
-    traffic_light TEXT NOT NULL,
+    traffic_light TEXT NOT NULL DEFAULT 'unknown',
     tags TEXT,
@@ -390,3 +390,3 @@
     nutrient_json TEXT,
-    histamine_score REAL DEFAULT 0,
+    histamine_score REAL,
     histamine_label TEXT DEFAULT 'unknown',
@@ -425,3 +425,3 @@
     canonical_food TEXT PRIMARY KEY,
-    personal_status TEXT NOT NULL DEFAULT 'unknown' CHECK(personal_status IN ('safe','problematic','unclear','unknown')),
+    personal_status TEXT NOT NULL DEFAULT 'unknown',
     evidence_level TEXT DEFAULT 'low' CHECK(evidence_level IN ('low','medium','high')),
@@ -435,2 +435,14 @@
     queue_key TEXT NOT NULL,
+    decision TEXT,
+    alias TEXT,
+    canonical_food TEXT,
+    sighi_score INTEGER,
+    confidence TEXT,
+    mapping_method TEXT,
+    source_label TEXT,
+    source_version TEXT,
+    note TEXT,
+    ingredient_review_required INTEGER,
+    personal_tolerance TEXT,
+    definition_version TEXT,
     applied_at TEXT DEFAULT CURRENT_TIMESTAMP
@@ -442,6 +454,16 @@
     normalized_name TEXT NOT NULL,
+    decision TEXT,
+    alias TEXT,
     canonical_food TEXT,
     sighi_score INTEGER,
-    traffic_light TEXT NOT NULL,
-    mapping_scope TEXT NOT NULL,
+    traffic_light TEXT,
+    mapping_scope TEXT,
+    confidence TEXT,
+    mapping_method TEXT,
+    source_label TEXT,
+    source_version TEXT,
+    note TEXT,
+    ingredient_review_required INTEGER,
+    personal_tolerance TEXT,
+    definition_version TEXT,
     affected_day TEXT NOT NULL,


Output:
Exit code: 0
Wall time: 0.1 seconds
Output:
Success. Updated the following files:
M database/schema.sql


Input:
*** Begin Patch
*** Update File: tests/fixtures/dashboard_v5_fixture.py
@@
-                (1, "synthetic cucumber", 0, "green", "fixture", "high", "complete synthetic mapping", fixture_timestamp),
+                (1, "synthetic cucumber", 0, "classified", "fixture", "high", "complete synthetic mapping", fixture_timestamp),
@@
             (_iso_day(current), None, None, None, None, 2, None, 0, 1, "unknown", json.dumps({"nutrient.protein": None, "nutrient.fiber": 4.2}), _iso_noon(current)),
         )
+        connection.executemany(
+            """INSERT INTO nutrition_item_nutrients(item_id,nutrient_key,value,unit,raw_value)
+               VALUES(?,?,?,?,?)""",
+            [
+                (1, "nutrient.fiber", 4.2, "g", "4.2"),
+                (1, "nutrient.sugar", 2.0, "g", "2.0"),
+                (1, "nutrient.vitamin_c", 20.0, "unknown", "20"),
+                (2, "nutrient.sodium", 10.0, "mg", "10"),
+            ],
+        )
@@
-            (_iso_day(current), "lunch", 0, 0, 0, 0, 1, "{}", 0, "green", _iso_noon(current)),
+            (_iso_day(current), "lunch", 0, 0, 0, 0, 1, "{}", 0, "classified", _iso_noon(current)),
*** End Patch


Success. Updated the following files:
M tests/fixtures/dashboard_v5_fixture.py

@@ -238,3 +238,3 @@
             [
-                (1, "synthetic cucumber", 0, "green", "fixture", "high", "complete synthetic mapping", fixture_timestamp),
+                (1, "synthetic cucumber", 0, "classified", "fixture", "high", "complete synthetic mapping", fixture_timestamp),
                 (2, None, None, "unknown", None, "low", "intentionally unmapped fixture row", fixture_timestamp),
@@ -249,2 +249,12 @@
         )
+        connection.executemany(
+            """INSERT INTO nutrition_item_nutrients(item_id,nutrient_key,value,unit,raw_value)
+               VALUES(?,?,?,?,?)""",
+            [
+                (1, "nutrient.fiber", 4.2, "g", "4.2"),
+                (1, "nutrient.sugar", 2.0, "g", "2.0"),
+                (1, "nutrient.vitamin_c", 20.0, "unknown", "20"),
+                (2, "nutrient.sodium", 10.0, "mg", "10"),
+            ],
+        )
         connection.execute(
@@ -253,3 +263,3 @@
                VALUES(?,?,?,?,?,?,?,?,?,?,?)""",
-            (_iso_day(current), "lunch", 0, 0, 0, 0, 1, "{}", 0, "green", _iso_noon(current)),
+            (_iso_day(current), "lunch", 0, 0, 0, 0, 1, "{}", 0, "classified", _iso_noon(current)),
         )


Output:
Exit code: 0
Wall time: 0.1 seconds
Output:
Success. Updated the following files:
M tests/fixtures/dashboard_v5_fixture.py


The synthetic fixture now includes known-unit nutrients and an unknown-unit micronutrient so the read API can prove omission behavior. Next I’m expanding the focused tests to cover CSRF/form validation, composite rejection, ignore idempotency, affected-day-only recompute, and missing nutrient behavior.


Input:
*** Begin Patch
*** Update File: tests/test_dashboard_v5_sprint6g_nutrition.py
@@
 import json
 import sqlite3
 import importlib.util
+import urllib.parse
 from datetime import date
 from pathlib import Path
@@
 def test_nutrition_read_apis_are_bounded_allowlisted_and_incomplete_not_zero(
@@
     assert days["definition"]["insufficient_inputs"] == "no_index"
+    assert "traffic_light" not in json.dumps(days)
     assert days["days"][-1]["histamine"]["score"] is None
     assert days["days"][-1]["histamine"]["status"] == "incomplete"
@@
     assert detail["coverage"]["status"] == "incomplete"
     assert detail["coverage"]["insufficient_inputs"] == "no_index"
+    assert detail["macros"]["kcal"] is None
     assert set(detail["nutrients"]) <= set(read_api.NUTRIENT_ALLOWLIST)
+    assert "nutrient.vitamin_c" not in detail["nutrients"]
+    assert {meal["meal"] for meal in detail["meals"]} >= {"breakfast", "lunch", "dinner", "snack", "unassigned"}
@@
 def test_mapping_worker_is_transactional_idempotent_and_recomputes_only_affected_days(
@@
         "version": 1,
         "action": "nutrition_mapping",
+        "decision": "assign",
         "queue_key": "synthetic_incomplete_food_mapping_with_a_very_long_name_that_must_wrap",
+        "alias": "SYNTHETIC_INCOMPLETE_FOOD_MAPPING_WITH_A_VERY_LONG_NAME_THAT_MUST_WRAP",
         "canonical_food": "synthetic reviewed food",
         "sighi_score": 1,
-        "traffic_light": "yellow",
-        "mapping_scope": "canonical",
-        "personal_tolerance": "unknown",
+        "confidence": "medium",
+        "mapping_method": "manual_review",
+        "source_label": "SIGHi",
+        "source_version": "2026-test",
+        "note": "synthetic reviewed mapping",
+        "ingredient_review_required": False,
+        "personal_tolerance": "documented local note",
     }
@@
-        day = connection.execute("SELECT histamine_unknown_count,histamine_score,histamine_label FROM nutrition_daily_summary_v2 WHERE datum='2026-06-14'").fetchone()
-        assert dict(day) == {"histamine_unknown_count": 0, "histamine_score": 1.0, "histamine_label": "yellow"}
+        day = connection.execute("SELECT kcal,protein_g,histamine_unknown_count,histamine_score,histamine_label,nutrient_json FROM nutrition_daily_summary_v2 WHERE datum='2026-06-14'").fetchone()
+        assert day["kcal"] is None and day["protein_g"] is None
+        assert day["histamine_unknown_count"] == 0
+        assert day["histamine_score"] == 1.0
+        assert day["histamine_label"] == "classified"
+        nutrients = json.loads(day["nutrient_json"])
+        assert nutrients["nutrient.fiber"] == 4.2
+        assert "nutrient.vitamin_c" not in nutrients
         assert connection.execute("SELECT COUNT(*) FROM nutrition_mapping_action_log").fetchone()[0] == 1
         assert connection.execute("SELECT COUNT(*) FROM nutrition_mapping_provenance WHERE affected_day='2026-06-14'").fetchone()[0] == 1
+        provenance = connection.execute("SELECT decision,alias,confidence,mapping_method,source_label,source_version,note,ingredient_review_required,personal_tolerance,definition_version FROM nutrition_mapping_provenance").fetchone()
+        assert dict(provenance) == {
+            "decision": "assign",
+            "alias": "SYNTHETIC_INCOMPLETE_FOOD_MAPPING_WITH_A_VERY_LONG_NAME_THAT_MUST_WRAP",
+            "confidence": "medium",
+            "mapping_method": "manual_review",
+            "source_label": "SIGHi",
+            "source_version": "2026-test",
+            "note": "synthetic reviewed mapping",
+            "ingredient_review_required": 0,
+            "personal_tolerance": "documented local note",
+            "definition_version": "sighi_mapping_load_v2",
+        }
         assert connection.execute("SELECT COUNT(*) FROM personal_food_tolerance").fetchone()[0] == 0
     finally:
         connection.close()
+
+
+def test_composite_and_ignore_do_not_alter_mappings(fixture_db: Path, monkeypatch: pytest.MonkeyPatch) -> None:
+    import health_dashboard_action_worker as worker
+
+    monkeypatch.setattr(worker, "DASHBOARD_DB", fixture_db)
+    monkeypatch.setattr(worker, "DASHBOARD_V5_FILE", None)
+    base = {
+        "version": 1,
+        "action": "nutrition_mapping",
+        "queue_key": "synthetic_incomplete_food_mapping_with_a_very_long_name_that_must_wrap",
+        "alias": "SYNTHETIC_INCOMPLETE_FOOD_MAPPING_WITH_A_VERY_LONG_NAME_THAT_MUST_WRAP",
+        "canonical_food": "",
+        "sighi_score": "unknown",
+        "confidence": "low",
+        "mapping_method": "manual_review",
+        "source_label": "SIGHi",
+        "source_version": "2026-test",
+        "note": "synthetic decision",
+        "ingredient_review_required": True,
+    }
+    with pytest.raises(ValueError):
+        worker.validate_mapping_payload({**base, "decision": "composite", "sighi_score": 1})
+    worker.apply_mapping_action({**base, "decision": "composite"}, "b" * 32)
+    worker.apply_mapping_action({**base, "decision": "ignore", "ingredient_review_required": False}, "c" * 32)
+
+    connection = sqlite3.connect(fixture_db)
+    try:
+        assert connection.execute("SELECT sighi_score FROM nutrition_histamine_scores WHERE item_id=2").fetchone()[0] is None
+        assert connection.execute("SELECT status FROM nutrition_composite_product_review").fetchone()[0] == "needs_ingredient_review"
+        assert connection.execute("SELECT COUNT(*) FROM nutrition_mapping_action_log").fetchone()[0] == 2
+    finally:
+        connection.close()
+
+
+def test_mapping_submission_requires_one_time_csrf_and_exact_shape(monkeypatch: pytest.MonkeyPatch) -> None:
+    import health_dashboard_server as server
+
+    monkeypatch.setattr(server, "local_today", lambda: TODAY)
+    token = server.issue_csrf_token(now=100.0)
+    form = {
+        "csrf_token": [token],
+        "return_to": ["v5"],
+        "decision": ["assign"],
+        "queue_key": ["synthetic_incomplete_food_mapping_with_a_very_long_name_that_must_wrap"],
+        "alias": ["SYNTHETIC_INCOMPLETE_FOOD_MAPPING_WITH_A_VERY_LONG_NAME_THAT_MUST_WRAP"],
+        "canonical_food": ["synthetic reviewed food"],
+        "sighi_score": ["1"],
+        "confidence": ["medium"],
+        "mapping_method": ["manual_review"],
+        "source_label": ["SIGHi"],
+        "source_version": ["2026-test"],
+        "note": ["bounded note"],
+        "ingredient_review_required": ["false"],
+        "personal_tolerance": ["neutral note"],
+    }
+    payload, return_to = server.validate_mapping_submission(form)
+    assert return_to == "v5"
+    assert payload["decision"] == "assign"
+    assert payload["personal_tolerance"] == "neutral note"
+    with pytest.raises(ValueError):
+        server.validate_mapping_submission({**form, "canonical_food": ["x", "y"]})
+    assert server.consume_csrf_token(token, now=101.0)
+    assert not server.consume_csrf_token(token, now=102.0)
*** End Patch


Success. Updated the following files:
M tests/test_dashboard_v5_sprint6g_nutrition.py

@@ -5,2 +5,3 @@
 import importlib.util
+import urllib.parse
 from datetime import date
@@ -68,2 +69,3 @@
     assert days["definition"]["insufficient_inputs"] == "no_index"
+    assert "traffic_light" not in json.dumps(days)
     assert days["days"][-1]["histamine"]["score"] is None
@@ -74,3 +76,6 @@
     assert detail["coverage"]["insufficient_inputs"] == "no_index"
+    assert detail["macros"]["kcal"] is None
     assert set(detail["nutrients"]) <= set(read_api.NUTRIENT_ALLOWLIST)
+    assert "nutrient.vitamin_c" not in detail["nutrients"]
+    assert {meal["meal"] for meal in detail["meals"]} >= {"breakfast", "lunch", "dinner", "snack", "unassigned"}
     assert all("id" not in item and "raw" not in json.dumps(item).casefold() for item in detail["items"])
@@ -94,8 +99,14 @@
         "action": "nutrition_mapping",
+        "decision": "assign",
         "queue_key": "synthetic_incomplete_food_mapping_with_a_very_long_name_that_must_wrap",
+        "alias": "SYNTHETIC_INCOMPLETE_FOOD_MAPPING_WITH_A_VERY_LONG_NAME_THAT_MUST_WRAP",
         "canonical_food": "synthetic reviewed food",
         "sighi_score": 1,
-        "traffic_light": "yellow",
-        "mapping_scope": "canonical",
-        "personal_tolerance": "unknown",
+        "confidence": "medium",
+        "mapping_method": "manual_review",
+        "source_label": "SIGHi",
+        "source_version": "2026-test",
+        "note": "synthetic reviewed mapping",
+        "ingredient_review_required": False,
+        "personal_tolerance": "documented local note",
     }
@@ -108,6 +119,25 @@
     try:
-        day = connection.execute("SELECT histamine_unknown_count,histamine_score,histamine_label FROM nutrition_daily_summary_v2 WHERE datum='2026-06-14'").fetchone()
-        assert dict(day) == {"histamine_unknown_count": 0, "histamine_score": 1.0, "histamine_label": "yellow"}
+        day = connection.execute("SELECT kcal,protein_g,histamine_unknown_count,histamine_score,histamine_label,nutrient_json FROM nutrition_daily_summary_v2 WHERE datum='2026-06-14'").fetchone()
+        assert day["kcal"] is None and day["protein_g"] is None
+        assert day["histamine_unknown_count"] == 0
+        assert day["histamine_score"] == 1.0
+        assert day["histamine_label"] == "classified"
+        nutrients = json.loads(day["nutrient_json"])
+        assert nutrients["nutrient.fiber"] == 4.2
+        assert "nutrient.vitamin_c" not in nutrients
         assert connection.execute("SELECT COUNT(*) FROM nutrition_mapping_action_log").fetchone()[0] == 1
         assert connection.execute("SELECT COUNT(*) FROM nutrition_mapping_provenance WHERE affected_day='2026-06-14'").fetchone()[0] == 1
+        provenance = connection.execute("SELECT decision,alias,confidence,mapping_method,source_label,source_version,note,ingredient_review_required,personal_tolerance,definition_version FROM nutrition_mapping_provenance").fetchone()
+        assert dict(provenance) == {
+            "decision": "assign",
+            "alias": "SYNTHETIC_INCOMPLETE_FOOD_MAPPING_WITH_A_VERY_LONG_NAME_THAT_MUST_WRAP",
+            "confidence": "medium",
+            "mapping_method": "manual_review",
+            "source_label": "SIGHi",
+            "source_version": "2026-test",
+            "note": "synthetic reviewed mapping",
+            "ingredient_review_required": 0,
+            "personal_tolerance": "documented local note",
+            "definition_version": "sighi_mapping_load_v2",
+        }
         assert connection.execute("SELECT COUNT(*) FROM personal_food_tolerance").fetchone()[0] == 0
@@ -115 +145,65 @@
         connection.close()
+
+
+def test_composite_and_ignore_do_not_alter_mappings(fixture_db: Path, monkeypatch: pytest.MonkeyPatch) -> None:
+    import health_dashboard_action_worker as worker
+
+    monkeypatch.setattr(worker, "DASHBOARD_DB", fixture_db)
+    monkeypatch.setattr(worker, "DASHBOARD_V5_FILE", None)
+    base = {
+        "version": 1,
+        "action": "nutrition_mapping",
+        "queue_key": "synthetic_incomplete_food_mapping_with_a_very_long_name_that_must_wrap",
+        "alias": "SYNTHETIC_INCOMPLETE_FOOD_MAPPING_WITH_A_VERY_LONG_NAME_THAT_MUST_WRAP",
+        "canonical_food": "",
+        "sighi_score": "unknown",
+        "confidence": "low",
+        "mapping_method": "manual_review",
+        "source_label": "SIGHi",
+        "source_version": "2026-test",
+        "note": "synthetic decision",
+        "ingredient_review_required": True,
+    }
+    with pytest.raises(ValueError):
+        worker.validate_mapping_payload({**base, "decision": "composite", "sighi_score": 1})
+    worker.apply_mapping_action({**base, "decision": "composite"}, "b" * 32)
+    worker.apply_mapping_action({**base, "decision": "ignore", "ingredient_review_required": False}, "c" * 32)
+
+    connection = sqlite3.connect(fixture_db)
+    try:
+        assert connection.execute("SELECT sighi_score FROM nutrition_histamine_scores WHERE item_id=2").fetchone()[0] is None
+        assert connection.execute("SELECT status FROM nutrition_composite_product_review").fetchone()[0] == "needs_ingredient_review"
+        assert connection.execute("SELECT COUNT(*) FROM nutrition_mapping_action_log").fetchone()[0] == 2
+    finally:
+        connection.close()
+
+
+def test_mapping_submission_requires_one_time_csrf_and_exact_shape(monkeypatch: pytest.MonkeyPatch) -> None:
+    import health_dashboard_server as server
+
+    monkeypatch.setattr(server, "local_today", lambda: TODAY)
+    token = server.issue_csrf_token(now=100.0)
+    form = {
+        "csrf_token": [token],
+        "return_to": ["v5"],
+        "decision": ["assign"],
+        "queue_key": ["synthetic_incomplete_food_mapping_with_a_very_long_name_that_must_wrap"],
+        "alias": ["SYNTHETIC_INCOMPLETE_FOOD_MAPPING_WITH_A_VERY_LONG_NAME_THAT_MUST_WRAP"],
+        "canonical_food": ["synthetic reviewed food"],
+        "sighi_score": ["1"],
+        "confidence": ["medium"],
+        "mapping_method": ["manual_review"],
+        "source_label": ["SIGHi"],
+        "source_version": ["2026-test"],
+        "note": ["bounded note"],
+        "ingredient_review_required": ["false"],
+        "personal_tolerance": ["neutral note"],
+    }
+    payload, return_to = server.validate_mapping_submission(form)
+    assert return_to == "v5"
+    assert payload["decision"] == "assign"
+    assert payload["personal_tolerance"] == "neutral note"
+    with pytest.raises(ValueError):
+        server.validate_mapping_submission({**form, "canonical_food": ["x", "y"]})
+    assert server.consume_csrf_token(token, now=101.0)
+    assert not server.consume_csrf_token(token, now=102.0)


Output:
Exit code: 0
Wall time: 0.1 seconds
Output:
Success. Updated the following files:
M tests/test_dashboard_v5_sprint6g_nutrition.py


rg -n "traffic_light|suggested_traffic_light|mapping_scope|personal_tolerance|safe|green|yellow|orange|red|Ampel" scripts/health tests/test_dashboard_v5_sprint6g_nutrition.py tests/fixtures/dashboard_v5_fixture.py docs/adr/0003-dietary-inflammation-model.md database/schema.sql -S


python -m py_compile scripts/health/health_dashboard_action_worker.py scripts/health/health_dashboard_server.py scripts/health/dashboard_v5/read_api.py scripts/health/dashboard_v5/data_provider.py scripts/health/dashboard_v5/contracts.py


Output:
Chunk ID: 02c8a4
Wall time: 0.0006 seconds
Process exited with code 0
Original token count: 262144
Output:
Total output lines: 446

docs/adr/0003-dietary-inflammation-model.md:23:The dashboard does not compute a dietary inflammation index when required inputs are insufficient. Unknown or unmapped foods make the histamine component incomplete rather than zero. Composite products require ingredient review before a definitive mapping is applied.
docs/adr/0003-dietary-inflammation-model.md:25:Personal tolerance labels remain separate from shared food mapping definitions.
tests/fixtures/dashboard_v5_fixture.py:44:    for predictor in SPRINT4B_PREDICTORS:
tests/fixtures/dashboard_v5_fixture.py:53:                if predictor == SPRINT4B_PREDICTORS[0] and lag == 0 and phase == SPRINT4B_PHASES[0]:
tests/fixtures/dashboard_v5_fixture.py:55:                elif predictor == SPRINT4B_PREDICTORS[0] and lag == 3 and phase == "early_treatment":
tests/fixtures/dashboard_v5_fixture.py:59:                elif predictor == SPRINT4B_PREDICTORS[1] and lag == 2 and phase == "stable_treatment":
tests/fixtures/dashboard_v5_fixture.py:64:                    "id": {"predictor": predictor, "target": "symptom_total"},
tests/fixtures/dashboard_v5_fixture.py:82:            "lag_direction": "predictor_d_to_target_d_plus_lag",
tests/fixtures/dashboard_v5_fixture.py:142:                ("HKQuantityType", "oxygen_saturation", _iso_noon(current), _iso_noon(current), None, "not measured", "%", "Synthetic Missing Value Source", "1", None, "synthetic-missing.json", "fixture-missing-file", "fixture-missing-record", "{}", _iso_noon(current)),
tests/fixtures/dashboard_v5_fixture.py:177:                   (predictor,target,lag_days,medication_phase,n,eligible_target_days,
tests/fixtures/dashboard_v5_fixture.py:182:                    result["id"]["predictor"], result["id"]["target"],
tests/fixtures/dashboard_v5_fixture.py:218:                (_iso_day(anchor_date - timedelta(days=4)), "SYNTHETIC_EVENT_ON_MEASUREMENT_GAP", "10 mg", "synthetic", "administered", None, "timeline gap fixture", "fixture", fixture_timestamp),
tests/fixtures/dashboard_v5_fixture.py:219:                (_iso_day(current), "SYNTHETIC_ADMINISTERED_MEDICATION", "10 mg", "synthetic", "administered", None, "display as administered only", "fixture", fixture_timestamp),
tests/fixtures/dashboard_v5_fixture.py:236:               (item_id,canonical_food,sighi_score,traffic_light,tags,confidence,reason,scored_at)
tests/test_dashboard_v5_sprint6g_nutrition.py:70:    assert "traffic_light" not in json.dumps(days)
tests/test_dashboard_v5_sprint6g_nutrition.py:110:        "ingredient_review_required": False,
tests/test_dashboard_v5_sprint6g_nutrition.py:111:        "personal_tolerance": "documented local note",
tests/test_dashboard_v5_sprint6g_nutrition.py:130:        provenance = connection.execute("SELECT decision,alias,confidence,mapping_method,source_label,source_version,note,ingredient_review_required,personal_tolerance,definition_version FROM nutrition_mapping_provenance").fetchone()
tests/test_dashboard_v5_sprint6g_nutrition.py:139:            "ingredient_review_required": 0,
tests/test_dashboard_v5_sprint6g_nutrition.py:140:            "personal_tolerance": "documented local note",
tests/test_dashboard_v5_sprint6g_nutrition.py:165:        "ingredient_review_required": True,
tests/test_dashboard_v5_sprint6g_nutrition.py:170:    worker.apply_mapping_action({**base, "decision": "ignore", "ingredient_review_required": False}, "c" * 32)
tests/test_dashboard_v5_sprint6g_nutrition.py:175:        assert connection.execute("SELECT status FROM nutrition_composite_product_review").fetchone()[0] == "needs_ingredient_review"
tests/test_dashboard_v5_sprint6g_nutrition.py:199:        "ingredient_review_required": ["false"],
tests/test_dashboard_v5_sprint6g_nutrition.py:200:        "personal_tolerance": ["neutral note"],
tests/test_dashboard_v5_sprint6g_nutrition.py:205:    assert payload["personal_tolerance"] == "neutral note"
database/schema.sql:172:    source TEXT DEFAULT 'local_seed_sighi_inspired',
database/schema.sql:324:    insight_type TEXT NOT NULL CHECK(insight_type IN ('trigger_candidate','safe_candidate','insufficient_data')),
database/schema.sql:340:    traffic_light TEXT NOT NULL DEFAULT 'unknown',
database/schema.sql:344:    scored_at TEXT DEFAULT CURRENT_TIMESTAMP,
database/schema.sql:407:    status TEXT DEFAULT 'open' CHECK(status IN ('open','mapped','ignored')),
database/schema.sql:445:    ingredient_review_required INTEGER,
database/schema.sql:446:    personal_tolerance TEXT,
database/schema.sql:459:    traffic_light TEXT,
database/schema.sql:460:    mapping_scope TEXT,
database/schema.sql:466:    ingredient_review_required INTEGER,
database/schema.sql:467:    personal_tolerance TEXT,
database/schema.sql:477:    status TEXT NOT NULL DEFAULT 'needs_ingredient_review' CHECK(status IN ('needs_ingredient_review','reviewed')),
database/schema.sql:483:    predictor TEXT NOT NULL,
database/schema.sql:500:    UNIQUE(predictor,target,lag_days,medication_phase)
scripts/health/health_dashboard_action_worker.py:55:MAPPING_METHODS = frozenset({"sighi_reference", "ingredient_label", "manual_review", "local_alias"})
scripts/health/health_dashboard_action_worker.py:112:    required = {
scripts/health/health_dashboard_action_worker.py:125:        "ingredient_review_required",
scripts/health/health_dashboard_action_worker.py:127:    optional = {"personal_tolerance"}
scripts/health/health_dashboard_action_worker.py:128:    if not required <= set(payload) <= required | optional:
scripts/health/health_dashboard_action_worker.py:142:    ingredient_review = payload["ingredient_review_required"]
scripts/health/health_dashboard_action_worker.py:143:    tolerance = payload.get("personal_tolerance")
scripts/health/health_dashboard_action_worker.py:158:        raise ValueError("required mapping documentation missing")
scripts/health/health_dashboard_action_worker.py:161:    if type(ingredient_review) is not bool:
scripts/health/health_dashboard_action_worker.py:162:        raise ValueError("invalid ingredient review flag")
scripts/health/health_dashboard_action_worker.py:166:        if not canonical or score == "unknown" or ingredient_review:
scripts/health/health_dashboard_action_worker.py:167:            raise ValueError("assign requires canonical score without ingredient review")
scripts/health/health_dashboard_action_worker.py:169:        if score != "unknown" or not ingredient_review:
scripts/health/health_dashboard_action_worker.py:170:            raise ValueError("composite must stay unclassified for ingredient review")
scripts/health/health_dashboard_action_worker.py:172:        if score != "unknown" or canonical or ingredient_review:
scripts/health/health_dashboard_action_worker.py:187:        "ingredient_review_required": ingredient_review,
scripts/health/health_dashboard_action_worker.py:188:        "personal_tolerance": tolerance,
scripts/health/health_dashboard_action_worker.py:246:        raise RuntimeError("HEALTH_DASHBOARD_DB is required")
scripts/health/health_dashboard_action_worker.py:337:            ingredient_review_required INTEGER,
scripts/health/health_dashboard_action_worker.py:338:            personal_tolerance TEXT,
scripts/health/health_dashboard_action_worker.py:355:            ingredient_review_required INTEGER,
scripts/health/health_dashboard_action_worker.py:356:            personal_tolerance TEXT,
scripts/health/health_dashboard_action_worker.py:365:            status TEXT NOT NULL DEFAULT 'needs_ingredient_review',
scripts/health/health_dashboard_action_worker.py:374:            "note": "TEXT", "ingredient_review_required": "INTEGER", "personal_tolerance": "TEXT",
scripts/health/health_dashboard_action_worker.py:380:            "ingredient_review_required": "INTEGER", "personal_tolerance": "TEXT",
scripts/health/health_dashboard_action_worker.py:456:        raise RuntimeError("HEALTH_DASHBOARD_DB is required")
scripts/health/health_dashboard_action_worker.py:487:                       VALUES(?,?, 'needs_ingredient_review', CURRENT_TIMESTAMP)""",
scripts/health/health_dashboard_action_worker.py:494:                           (item_id,canonical_food,sighi_score,traffic_light,tags,confidence,reason,scored_at)
scripts/health/health_dashboard_action_worker.py:500:                connection.execute("UPDATE nutrition_review_queue SET status='ignored',updated_at=CURRENT_TIMESTAMP WHERE normalized_name=?", (normalized,))
scripts/health/health_dashboard_action_worker.py:507:                        mapping_method,source_label,source_version,note,ingredient_review_required,
scripts/health/health_dashboard_action_worker.py:508:                        personal_tolerance,definition_version,affected_day)
scripts/health/health_dashboard_action_worker.py:513:                        payload["source_version"], payload["note"], int(payload["ingredient_review_required"]),
scripts/health/health_dashboard_action_worker.py:514:                        payload.get("personal_tolerance"), HISTAMINE_DEFINITION_VERSION, str(day),
scripts/health/health_dashboard_action_worker.py:520:                    mapping_method,source_label,source_version,note,ingredient_review_required,
scripts/health/health_dashboard_action_worker.py:521:                    personal_tolerance,definition_version)
scripts/health/health_dashboard_action_worker.py:527:                    int(payload["ingredient_review_required"]), payload.get("personal_tolerance"),
scripts/health/health_dashboard_action_worker.py:581:        print("HEALTH_DASHBOARD_DB is required", file=sys.stderr)
scripts/health/health_add_event_period.py:17:    ap.add_argument("--start", required=True, help="YYYY-MM-DD")
scripts/health/health_add_event_period.py:19:    ap.add_argument("--type", required=True, help="z.B. Aphte, Schub, Infekt, Medikation, Ernährung")
scripts/health/apple_health_drive_sync.py:5:Requires gog CLI credentials for friday.uplink@gmail.com.
scripts/health/apple_health_drive_sync.py:28:        raise RuntimeError(f"Unsafe permissions on gog keyring secret file: {GOG_SECRET_ENV}")
scripts/health/generate_doctor_report.py:52:        raise RuntimeError(f"Unsafe permissions on gog keyring secret file: {GOG_SECRET_ENV}")
scripts/health/generate_doctor_report.py:122:    safe = str(text or "").replace("&", "&").replace("<", "<").replace(">", ">")
scripts/health/generate_doctor_report.py:123:    return Paragraph(safe, ST[style])
scripts/health/generate_doctor_report.py:266:    story.append(Paragraph("Hinweis: Strukturierter Gesprächsbericht; ersetzt keine ärztliche Beurteilung. Es erfolgt keine automatische medizinische Risiko-, Ampel- oder Entwarnungsbewertung. Angezeigte Laborwerte sind validiert, am Original geprüft und verwenden den dokumentbezogenen Referenzbereich; die ärztliche Interpretation bleibt erforderlich.", ST["Small"]))
scripts/health/document_fts_migrate.py:119:    parser.add_argument("--db", type=Path, required=True)
scripts/health/document_fts_migrate.py:120:    mode = parser.add_mutually_exclusive_group(required=True)
scripts/health/health_dashboard_v4.py:119:        or label not in {"green", "yellow", "orange", "red"}
scripts/health/health_dashboard_v4.py:126:    traffic = str(row["traffic_light"] or "").strip().casefold()
scripts/health/health_dashboard_v4.py:134:        traffic not in {"green", "yellow", "orange", "red"}
scripts/health/health_dashboard_v4.py:144:def is_administered_event(event_type: Any) -> bool:
scripts/health/health_dashboard_v4.py:145:    return str(event_type or "").strip().casefold() in {"administered", "verabreicht"}
scripts/health/health_dashboard_v4.py:271:        "orange",
scripts/health/health_dashboard_v4.py:371:                   h.traffic_light,h.canonical_food,h.sighi_score,h.tags,h.confidence
scripts/health/health_dashboard_v4.py:377:        administered_medications = [
scripts/health/health_dashboard_v4.py:380:            if is_administered_event(row["event_type"])
scripts/health/health_dashboard_v4.py:385:                SELECT predictor,target,lag_days,medication_phase,n,eligible_target_days,
scripts/health/health_dashboard_v4.py:389:                         COALESCE(q_value, 2), predictor, lag_days, medication_phase
scripts/health/health_dashboard_v4.py:394:        personal_tolerances = list(c.execute("SELECT canonical_food,personal_status,evidence_level,notes,updated_at FROM personal_food_tolerance ORDER BY CASE personal_status WHEN 'problematic' THEN 1 WHEN 'unclear' THEN 2 WHEN 'safe' THEN 3 ELSE 4 END, canonical_food"))
scripts/health/health_dashboard_v4.py:515:    light_icon = {"green":"🟢", "yellow":"🟡", "orange":"🟠", "red":"🔴", "unknown":"⚫"}
scripts/health/health_dashboard_v4.py:637:            f"
{html.escape(str(r['predictor']))}	"
scripts/health/health_dashboard_v4.py:651:        'safe': 'bisher persönlich dokumentiert vertragen',
scripts/health/health_dashboard_v4.py:658:        for r in personal_tolerances
scripts/health/health_dashboard_v4.py:689:        for r in administered_medications[:8]
scripts/health/health_dashboard_v4.py:698:    for r in administered_medications[:20]:
scripts/health/health_dashboard_v4.py:727:    medication_days = {str(row['datum'])[:10] for row in administered_medications if row['datum']}
scripts/health/health_dashboard_v4.py:754:        warnings.append(('yellow','Dokumente ungeprüft', f'{unreviewed} Dokumente haben noch keinen Review-Status.'))
scripts/health/health_dashboard_v4.py:756:        warnings.append(('yellow','Symptomdaten unzureichend', f'Nur {len(complete_symptom_days)} vollständig dokumentierte Tage; die neue Engine berechnet erst ab 14 vollständigen Paaren und wertet Signale erst ab 30 Paaren als korrigierte Hypothese.'))
scripts/health/health_dashboard_v4.py:757:    red_recent = c_ext.execute("SELECT COUNT(*) FROM nutrition_daily_summary_v2 WHERE histamine_label='red' AND date(datum) >= date('now','-30 day')").fetchone()[0]
scripts/health/health_dashboard_v4.py:758:    if red_recent:
scripts/health/health_dashboard_v4.py:759:        warnings.append(('orange','Hohe Histamin-Tage', f'{red_recent} rote Tage in den letzten 30 Tagen.'))
scripts/health/health_dashboard_v4.py:800:        css = "green" if status == "aktuell" else "yellow" if status == "prüfen" else "unknown"
scripts/health/health_dashboard_v4.py:869:function openSection(id) {{ const el=document.getElementById(id); if(!el) return; if(el.tagName.toLowerCase()==='details') el.open=true; el.scrollIntoView({{behavior:window.matchMedia('(prefers-reduced-motion: reduce)').matches?'auto':'smooth',block:'start'}}); }}
scripts/health/health_dashboard_v4.py:893:*{{box-sizing:border-box}}body{{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Arial,sans-serif;margin:18px;background:var(--bg);color:var(--text);font-size:15px;line-height:1.35}}a{{color:var(--blue)}}h1{{font-size:1.55rem;margin:.2rem 0}}h2{{font-size:1.25rem;margin:1.25rem 0 .6rem}}.grid{{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px}}.card,.chart-card,.day-card{{background:white;border-radius:14px;padding:14px;box-shadow:0 2px 8px #0001;margin-bottom:14px}}.num{{font-size:26px;font-weight:700;color:var(--blue)}}table{{border-collapse:collapse;width:100%;background:white;margin-bottom:14px;display:block;overflow-x:auto;-webkit-overflow-scrolling:touch}}td,th{{border:1px solid #ddd;padding:6px;vertical-align:top;white-space:nowrap}}code{{white-space:pre-wrap;overflow-wrap:anywhere;word-break:break-word}}th{{background:var(--blue);color:white}}canvas{{background:white;border-radius:12px;margin-bottom:12px;width:100%!important;max-height:330px}}.muted{{color:var(--muted);font-size:.9em;margin-bottom:8px}}.warn{{background:#fff7ed;border-left:4px solid #f97316;padding:10px;border-radius:8px}}.button{{display:inline-block;background:var(--blue);color:white!important;text-decoration:none;padding:10px 14px;border-radius:8px;margin:4px 8px 4px 0}}.pill{{display:inline-flex;align-items:center;gap:3px;border-radius:999px;padding:3px 8px;font-size:.82em;font-weight:700;white-space:nowrap}}.green{{background:#dcfce7;color:#166534}}.yellow{{background:#fef9c3;color:#854d0e}}.orange{{background:#ffedd5;color:#9a3412}}.red{{background:#fee2e2;color:#991b1b}}.unknown{{background:#e5e7eb;color:#374151}}.day-list{{display:grid;grid-template-columns:1fr;gap:10px}}.day-card{{padding:0;overflow:hidden}}.day-summary{{display:grid;grid-template-columns:1.25fr auto auto auto auto;gap:8px;align-items:center;cursor:pointer;padding:14px}}.day-summary::-webkit-details-marker{{display:none}}.day-summary:before{{content:'▸';font-size:14px;color:var(--muted)}}.day-card[open]>.day-summary:before{{content:'▾'}}.day-date{{font-weight:800}}.day-kcal{{font-weight:800;color:var(--blue)}}.day-load,.day-items{{color:var(--muted);font-size:.9em}}.day-detail{{border-top:1px solid var(--line);padding:12px 14px}}.day-macro{{font-weight:600;margin-bottom:10px;color:#374151}}.meal-card{{border:1px solid var(--line);border-radius:12px;padding:12px;margin:10px 0;background:#fbfdff}}.meal-head{{display:flex;justify-content:space-between;align-items:center;gap:8px}}.meal-head h4{{margin:0;font-size:1rem}}.macroline{{margin:6px 0 8px;color:#374151}}.food-list{{display:grid;gap:6px}}.food-row{{display:grid;grid-template-columns:24px minmax(0,1fr) 52px 60px;grid-template-areas:'h name amount kcal' '. name macro macro';gap:2px 8px;align-items:center;padding:8px;border:1px solid var(--line);border-radius:10px;background:white}}.food-h{{grid-area:h}}.food-name{{grid-area:name;font-weight:650;min-width:0;overflow-wrap:anywhere}}.food-name small{{display:block;color:var(--muted);font-weight:400}}.food-amount{{grid-area:amount;text-align:right;color:#374151}}.food-kcal{{grid-area:kcal;text-align:right;font-weight:700}}.food-macro{{grid-area:macro;color:var(--muted);font-size:.86em;text-align:right}}.chart-card table,.card table{{font-size:.92em}}.topbar{{position:sticky;top:0;z-index:20;background:rgba(246,248,251,.96);backdrop-filter:blur(8px);padding:8px 0 10px;border-bottom:1px solid var(--line);margin-bottom:12px}}.quicklinks{{display:flex;gap:8px;overflow-x:auto;-webkit-overflow-scrolling:touch;padding:4px 0}}.quicklinks a,.quicklinks button{{white-space:nowrap;border:1px solid var(--line);background:white;color:var(--blue);padding:8px 10px;border-radius:999px;text-decoration:none;font-weight:700}}.searchbox{{width:100%;border:1px solid var(--line);border-radius:12px;padding:11px 12px;font-size:16px;background:white}}.kpi-grid{{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:10px}}.mini-card{{background:white;border:1px solid var(--line);border-radius:14px;padding:12px;display:flex;flex-direction:column;gap:5px;min-height:88px}}.mini-card b{{font-size:.95rem}}.mini-card .big{{font-size:1.8rem;font-weight:800;color:var(--blue)}}.mini-card small{{color:var(--muted)}}.section-block{{background:white;border-radius:14px;box-shadow:0 2px 8px #0001;margin:14px 0;overflow:hidden}}.section-block>summary{{cursor:pointer;list-style:none;padding:16px;font-size:1.12rem;font-weight:800;color:var(--blue);border-bottom:1px solid var(--line)}}.section-block>summary::-webkit-details-marker{{display:none}}.section-block>summary:before{{content:'▸ ';color:var(--muted)}}.section-block[open]>summary:before{{content:'▾ '}}.section-body{{padding:14px}}.priority{{border-left:5px solid var(--blue)}}.legacy-note{{font-size:.85em;color:var(--muted)}}.info-list{{display:grid;gap:8px}}.info-row{{border:1px solid var(--line);border-radius:12px;padding:10px;background:#fff;display:grid;gap:3px}}.info-row b{{overflow-wrap:anywhere}}.info-row span{{font-weight:800;color:var(--blue)}}.info-row small{{color:var(--muted);overflow-wrap:anywhere}}
scripts/health/health_dashboard_v4.py:895:.skip-link{{position:absolute;left:-9999px;top:8px;background:#111827;color:#fff;padding:10px 14px;z-index:999;border-radius:8px}}.skip-link:focus-visible{{left:8px}}a,button,input,select,textarea,summary{{min-height:44px;touch-action:manipulation}}in…252144 tokens truncated…ransform:lK,parseAxisBreakOptionInwardTransform:uK,makeAxisLabelFormatterParamBreak:hK},Hp||(Hp=t)}var pK=Pa();function fK(t,e,n,i,r){var o=n.axis;if(!o.scale.isBlank()&&Wp()){var a=Wp().retrieveAxisBreakPairs(o.scale.getTicks({breakTicks:"only_break"}),function(t){return t.break},!1);if(a.length){var s=n.getModel("breakArea"),l=s.get("zigzagAmplitude"),u=s.get("zigzagMinSpan"),c=s.get("zigzagMaxSpan");u=Math.max(2,u||0),c=Math.max(u,c||0);var h=s.get("expandOnClick"),d=s.get("zigzagZ"),p=s.getModel("itemStyle").getItemStyle(),f=p.stroke,g=p.lineWidth,v=p.lineDash,y=p.fill,m=new vo({ignoreModelZ:!0}),x=o.isHorizontal(),_=pK(e).visualList||(pK(e).visualList=[]);B(_,function(t){return t.shouldRemove=!0});for(var b=function(t){var e=a[t][0].break.parsedBreak,s=[];s[0]=o.toGlobalCoord(o.dataToCoord(e.vmin,!0)),s[1]=o.toGlobalCoord(o.dataToCoord(e.vmax,!0)),s[1]<s[0]&&s.reverse();var b=function(t,e){var n=H(t,function(t){return Wp().identifyAxisBreak(t.parsedBreak.breakOption,e.breakOption)});return n||t.push(n={zigzagRandomList:[],parsedBreak:e,shouldRemove:!1}),n}(_,e);b.shouldRemove=!1;var w=new vo;!function(t,e,n,r,o,a){var s={stroke:f,lineWidth:g,lineDash:v,fill:"none"},h=o?0:1,m=1-h,x=i[dd[m]]+i[pd[m]];function _(t){var e=[],n=[];e[h]=n[h]=t,e[m]=i[dd[m]],n[m]=x;var r={x1:e[0],y1:e[1],x2:n[0],y2:n[1]};return iu(r,r,{lineWidth:1}),e[0]=r.x1,e[1]=r.y1,e[h]}n=_(n),r=_(r);for(var b=[],w=[],S=!0,M=i[dd[m]],I=0;;I++){var T=M===i[dd[m]],C=M>=x;C&&(M=x);var D=[],A=[];D[h]=n,A[h]=r,T||C||(D[h]+=S?-l:l,A[h]-=S?l:-l),D[m]=M,A[m]=M,b.push(D),w.push(A);var k=void 0;if(I<t.length?k=t[I]:(k=Math.random(),t.push(k)),M+=k*(c-u)+u,S=!S,C)break}var L=Wp().serializeAxisBreakIdentifier(a.breakOption);if(e.add(new kh({anid:"break_a_"+L,shape:{points:b},style:s,z:d})),0!==a.gapReal){e.add(new kh({anid:"break_b_"+L,shape:{points:w},style:s,z:d}));var P=w.slice();P.reverse();var O=b.concat(P);e.add(new Dh({anid:"break_c_"+L,shape:{points:O},style:{fill:y,opacity:p.opacity},z:d}))}}(b.zigzagRandomList,w,s[0],s[1],x,e),h&&w.on("click",function(){var t={type:ZC,breaks:[{start:e.breakOption.start,end:e.breakOption.end}]};t[o.dim+"AxisIndex"]=n.componentIndex,r.dispatchAction(t)}),w.silent=!h,m.add(w)},w=0;w=0;e--)t[e].shouldRemove&&t.splice(e,1)}(_)}}}function gK(t,e,n,i){var r=t.axis,o=n.transform;ct(i.style);var a=r.getExtent();r.inverse&&(a=a.slice()).reverse();var s=V(Wp().retrieveAxisBreakPairs(r.scale.getTicks({breakTicks:"only_break"}),function(t){return t.break},!1),function(t){var e=t[0].break.parsedBreak,n=[r.dataToCoord(e.vmin,!0),r.dataToCoord(e.vmax,!0)];return n[0]>n[1]&&n.reverse(),{coordPair:n,brkId:Wp().serializeAxisBreakIdentifier(e.breakOption)}});s.sort(function(t,e){return t.coordPair[0]-e.coordPair[0]});for(var l=a[0],u=null,c=0;c<s.length;c++){var h=s[c],d=Math.max(h.coordPair[0],a[0]),p=Math.min(h.coordPair[1],a[1]);l<=d&&f(l,d,u,h),l=p,u=h}function f(t,n,r,a){function s(t,e){o&&(Yt(t,t,o),Yt(e,e,o))}function l(t,e){var n={x1:t[0],y1:t[1],x2:e[0],y2:e[1]};iu(n,n,i.style),t[0]=n.x1,t[1]=n.y1,e[0]=n.x2,e[1]=n.y2}var u=[t,0],c=[n,0],h=[t,5],d=[n,5];s(u,h),l(u,h),s(c,d),l(c,d),l(u,c);var p=new Oh(k({shape:{x1:u[0],y1:u[1],x2:c[0],y2:c[1]}},i));e.add(p),p.anid="breakLine_"+(r?r.brkId:"\0")+"_\0_"+(a?a.brkId:"\0")}l<=a[1]&&f(l,a[1],u,null)}function vK(t,e,n){if(!H(n,function(t){return!t})){var i=new Pe;if(aI(n[0],n[1],i,{direction:-(t?e+Math.PI:e),touchThreshold:0,bidirectional:!1})){var r=[1,0,0,1,0,0];Ce(r,r,-e);var o=V(n,function(t){return t.transform?Ie([1,0,0,1,0,0],r,t.transform):r}),a=.5;if(g(0)||g(1)){var s=V(n,function(t,e){var n=t.localRect.clone();return n.applyTransform(o[e]),n}),l=new Pe;l.copy(n[0].label).add(n[1].label).scale(.5),l.transform(r);var u=i.clone().transform(r),c=(s[0].x+s[1].x+(u.x>=0?s[0].width:s[1].width)+u.x)/2-l.x,h=Math.min(c,c-u.x),d=Math.max(c,c-u.x);a=(c-(d<0?d:h>0?h:0))/u.x}var p=new Pe,f=new Pe;Pe.scale(p,i,-a),Pe.scale(f,i,1-a),eI(n[0],p),eI(n[1],f)}}function g(t){var e=n[0].localRect,i=new Pe(e[pd[t]]*o[0][0],e[pd[t]]*o[0][1]);return Math.abs(i.y)<1e-5}}function yK(t,e){var n={breaks:[]};return B(e.breaks,function(i){if(i){var r=H(t.get("breaks",!0),function(t){return Wp().identifyAxisBreak(t,i)});if(r){var o=e.type,a={isExpanded:!!r.isExpanded};r.isExpanded=o===ZC||o!==jC&&(o===qC?!r.isExpanded:r.isExpanded),n.breaks.push({start:r.start,end:r.end,isExpanded:!!r.isExpanded,old:a})}}}),n}function mK(){var t;t={adjustBreakLabelPair:vK,buildAxisBreakLine:gK,rectCoordBuildBreakAxis:fK,updateModelAxisBreak:yK},YC||(YC=t)}function xK(t,e){B(t,function(t){if(!t.model.get(["axisLabel","inside"])){var n=function(t){var e,n,i=t.model,r=t.scale;if(!i.get(["axisLabel","show"])||r.isBlank())return;var o=r.getExtent();n=r instanceof Mw?r.count():(e=r.getTicks()).length;var a,s=t.getLabelModel(),l=Zw(t),u=1;n>40&&(u=Math.ceil(n/40));for(var c=0;c<n;c+=u){var h=l(e?e[c]:{value:o[0]+c},c),d=p(s.getTextRect(h),s.get("rotate")||0);a?a.union(d):a=d}return a;function p(t,e){var n=e*Math.PI/180,i=t.width,r=t.height,o=i*Math.abs(Math.cos(n))+Math.abs(r*Math.sin(n)),a=i*Math.abs(Math.sin(n))+Math.abs(r*Math.cos(n));return new Xe(t.x,t.y,o,a)}}(t);if(n){var i=t.isHorizontal()?"height":"width",r=t.model.get(["axisLabel","margin"]);e[i]-=n[i]+r,"top"===t.position?e.y+=n.height+r:"left"===t.position&&(e.x+=n.width+r)}}})}gb([function(t){t.registerPainter("canvas",XT)}]),gb([function(t){t.registerPainter("svg",DT)}]),gb([function(t){t.registerChartView(VC),t.registerSeriesModel(ZT),t.registerLayout(GC("line",!0)),t.registerVisual({seriesType:"line",reset:function(t){var e=t.getData(),n=t.getModel("lineStyle").getLineStyle();n&&!n.stroke&&(n.stroke=e.getVisual("style").fill),e.setVisual("legendLineStyle",n)}}),t.registerProcessor(t.PRIORITY.PROCESSOR.STATISTIC,WC("line"))},function(t){t.registerChartView(rA),t.registerSeriesModel(KD),t.registerLayout(t.PRIORITY.VISUAL.LAYOUT,YD(PD)),t.registerLayout(t.PRIORITY.VISUAL.PROGRESSIVE_LAYOUT,XD(PD)),t.registerProcessor(t.PRIORITY.PROCESSOR.STATISTIC,WC(PD)),t.registerAction({type:"changeAxisOrder",event:"changeAxisOrder",update:"update"},function(t,e){var n=t.componentType||"series";e.eachComponent({mainType:n,query:t},function(e){t.sortInfo&&e.axis.setCategorySortInfo(t.sortInfo)})}),jD(t)},function(t){t.registerChartView(GA),t.registerSeriesModel(TA),$m(MA,t.registerAction),t.registerLayout(zA),t.registerProcessor(_A(MA)),t.registerProcessor(function(t){return{seriesType:t,reset:function(t,e){var n=t.getData();n.filterSelf(function(t){var e=n.mapDimension("value"),i=n.get(e,t);return!(K(i)&&!isNaN(i)&&i<0)})}}}(MA))},function(t){gb(Ok),t.registerSeriesModel(FA),t.registerChartView(YA),t.registerLayout(GC("scatter"))},function(t){gb(eL),t.registerChartView(Yk),t.registerSeriesModel(Uk),t.registerLayout(nL),t.registerProcessor(_A("radar")),t.registerPreprocessor(Hk)},function(t){gb(TO),t.registerChartView(gO),t.registerSeriesModel(cO),t.registerLayout(CO),t.registerProcessor(t.PRIORITY.PROCESSOR.STATISTIC,DO),$m("map",t.registerAction)},function(t){t.registerChartView(iR),t.registerSeriesModel(tR),t.registerLayout(hR),t.registerVisual(dR),function(t){t.registerAction({type:"treeExpandAndCollapse",event:"treeExpandAndCollapse",update:"update"},function(t,e){e.eachComponent({mainType:Du,subType:QO,query:t},function(e){var n=t.dataIndex,i=e.getData().tree.getNodeByDataIndex(n);i.isExpand=!i.isExpand})}),UP(t,Du,QO)}(t)},function(t){t.registerSeriesModel(gR),t.registerChartView(WR),t.registerVisual(aN),t.registerLayout(DR),function(t){for(var e=0;e<pR.length;e++)t.registerAction({type:pR[e],update:"updateView"},St);t.registerAction({type:"treemapRootToNode",update:"updateView"},function(t,e){e.eachComponent({mainType:"series",subType:"treemap",query:t},function(e,n){var i=qO(t,["treemapZoomToNode","treemapRootToNode"],e);if(i){var r=e.getViewRoot();r&&(t.direction=$O(r,i.node)?"rollUp":"drillDown"),e.resetViewRoot(i.node)}})})}(t)},function(t){t.registerChartView(pE),t.registerSeriesModel(IN),t.registerProcessor(fE),t.registerVisual(gE),t.registerVisual(CN),t.registerLayout(kN),t.registerLayout(t.PRIORITY.VISUAL.POST_CHART_LAYOUT,BN),t.registerLayout(GN),t.registerCoordinateSystem("graphView",{dimensions:nP.dimensions,create:FN}),t.registerAction({type:"focusNodeAdjacency",event:"focusNodeAdjacency",update:"series:focusNodeAdjacency"},St),t.registerAction({type:"unfocusNodeAdjacency",event:"unfocusNodeAdjacency",update:"series:unfocusNodeAdjacency"},St),UP(t,Du,MN)},function(t){t.registerChartView(wE),t.registerSeriesModel(yE),t.registerLayout(t.PRIORITY.VISUAL.POST_CHART_LAYOUT,ME),t.registerProcessor(_A("chord"))},function(t){t.registerChartView(DE),t.registerSeriesModel(AE)},function(t){t.registerChartView(RE),t.registerSeriesModel(LE),t.registerLayout(NE),t.registerProcessor(_A(kE))},function(t){gb(iB),t.registerChartView(zE),t.registerSeriesModel(WE),t.registerVisual(t.PRIORITY.VISUAL.BRUSH,XE)},function(t){t.registerChartView(lB),t.registerSeriesModel(oB),t.registerLayout(cB),t.registerVisual(wB),t.registerAction({type:"dragNode",event:"dragnode",update:"update"},function(t,e){e.eachComponent({mainType:Du,subType:rB,query:t},function(e){e.setNodePosition(t.dataIndex,[t.localX,t.localY])})}),UP(t,Du,rB)},function(t){t.registerSeriesModel(TB),t.registerChartView(CB),t.registerLayout(RB),t.registerTransform(NB),function(t){OB(t,function(){var e=AD(IB);xS(t,{key:e,seriesType:IB,getMetrics:LD}),AS(e,DD(e))})}(t)},function(t){t.registerChartView(ZB),t.registerSeriesModel(zB),t.registerPreprocessor(rV),t.registerVisual(YB),t.registerLayout(aV),function(t){oV(t,function(){var e=AD(EB);xS(t,{key:e,seriesType:EB,getMetrics:LD}),AS(e,DD(e))})}(t)},function(t){t.registerChartView(cV),t.registerSeriesModel(dV),t.registerLayout(GC("effectScatter"))},function(t){t.registerChartView(_V),t.registerSeriesModel(MV),t.registerLayout(xV),t.registerVisual(TV)},function(t){t.registerChartView(DV),t.registerSeriesModel(AV)},function(t){t.registerChartView(OV),t.registerSeriesModel($V),t.registerLayout(t.PRIORITY.VISUAL.LAYOUT,YD(OD)),t.registerLayout(t.PRIORITY.VISUAL.PROGRESSIVE_LAYOUT,XD(OD)),jD(t)},function(t){t.registerChartView(tG),t.registerSeriesModel(QV),t.registerLayout(eG),t.registerProcessor(_A(JV))},function(t){t.registerChartView(uG),t.registerSeriesModel(oG),t.registerLayout(dG),t.registerVisual(cG),function(t){t.registerAction({type:sG,update:"updateView"},function(t,e){e.eachComponent({mainType:"series",subType:"sunburst",query:t},function(e,n){var i=qO(t,[sG],e);if(i){var r=e.getViewRoot();r&&(t.direction=$O(r,i.node)?"rollUp":"drillDown"),e.resetViewRoot(i.node)}})}),t.registerAction({type:lG,update:"none"},function(t,e,n){t=k({},t),e.eachComponent({mainType:"series",subType:"sunburst",query:t},function(e){var n=qO(t,[lG],e);n&&(t.dataIndex=n.node.dataIndex)}),n.dispatchAction(k(t,{type:"highlight"}))}),t.registerAction({type:"sunburstUnhighlight",update:"updateView"},function(t,e,n){t=k({},t),n.dispatchAction(k(t,{type:"downplay"}))})}(t)},function(t){t.registerChartView(QG),t.registerSeriesModel(yG)}]),gb(function(t){gb(Ok),gb(nH)}),gb(function(t){gb(nH),wk.registerAxisPointerClass("PolarAxisPointer",iH),t.registerCoordinateSystem("polar",xH),t.registerComponentModel(sH),t.registerComponentView(RH),JA(t,"angle",uH,PH),JA(t,"radius",cH,OH),t.registerComponentView(MH),t.registerComponentView(CH),t.registerLayout(LH),function(t,e){AH(t,function(){var n=kD(e,oH);RD(t,n,e,oH),AS(n,DD(n))})}(t,PD)}),gb(TO),gb(function(t){gb(nH),wk.registerAxisPointerClass("SingleAxisPointer",XH),t.registerComponentView(KH),t.registerComponentView(zH),t.registerComponentModel(Ek),JA(t,"single",Ek,Ek.defaultOption),t.registerCoordinateSystem("single",WH)}),gb(iB),gb(function(t){t.registerComponentModel($H),t.registerComponentView(QH),t.registerCoordinateSystem("calendar",eW)}),gb(function(t){t.registerComponentModel(DW),t.registerComponentView(OW),t.registerCoordinateSystem("matrix",FW)}),gb(function(t){t.registerComponentModel(lU),t.registerComponentView(hU),t.registerPreprocessor(function(t){var e=t.graphic;X(e)?e[0]&&e[0].elements?t.graphic=[t.graphic[0]]:t.graphic=[{elements:e}]:e&&!e.elements&&(t.graphic=[{elements:[e]}])})}),gb(function(t){t.registerComponentModel(GU),t.registerComponentView(HU),BU("saveAsImage",UU),BU("magicType",ZU),BU("dataView",tY),BU("dataZoom",SY),BU("restore",oY),gb(NU)}),gb(function(t){gb(nH),t.registerComponentModel(TY),t.registerComponentView(WY),t.registerAction({type:"showTip",event:"showTip",update:"tooltip:manuallyShowTip"},St),t.registerAction({type:"hideTip",event:"hideTip",update:"tooltip:manuallyHideTip"},St)}),gb(nH),gb(function(t){t.registerComponentView(hX),t.registerComponentModel(dX),t.registerPreprocessor(jY),t.registerVisual(t.PRIORITY.VISUAL.BRUSH,aX),t.registerAction({type:"brush",event:"brush",update:"updateVisual"},function(t,e){e.eachComponent({mainType:"brush",query:t},function(e){e.setAreas(t.areas)})}),t.registerAction({type:"brushSelect",event:"brushSelected",update:"none"},St),t.registerAction({type:"brushEnd",event:"brushEnd",update:"none"},St),BU("brush",gX)}),gb(function(t){t.registerComponentModel(vX),t.registerComponentView(yX)}),gb(function(t){t.registerComponentModel(xX),t.registerComponentView(MX),t.registerSubTypeDefaulter("timeline",function(){return"slider"}),function(t){t.registerAction({type:"timelineChange",event:"timelineChanged",update:"prepareAndUpdate"},function(t,e,n){var i=e.getComponent("timeline");return i&&null!=t.currentIndex&&(i.setCurrentIndex(t.currentIndex),!i.get("loop",!0)&&i.isIndexMax()&&i.getPlayState()&&(i.setPlayState(!1),n.dispatchAction({type:"timelinePlayChange",playState:!1,from:t.from}))),e.resetOption("timeline",{replaceMerge:i.get("replaceMerge",!0)}),P({currentIndex:i.option.currentIndex},t)}),t.registerAction({type:"timelinePlayChange",event:"timelinePlayChanged",update:"update"},function(t,e){var n=e.getComponent("timeline");n&&null!=t.playState&&n.setPlayState(t.playState)})}(t),t.registerPreprocessor(CX)}),gb(function(t){t.registerComponentModel(RX),t.registerComponentView(XX),t.registerPreprocessor(function(t){kX(t.series,"markPoint")&&(t.markPoint=t.markPoint||{})})}),gb(function(t){t.registerComponentModel(ZX),t.registerComponentView(tZ),t.registerPreprocessor(function(t){kX(t.series,"markLine")&&(t.markLine=t.markLine||{})})}),gb(function(t){t.registerComponentModel(eZ),t.registerComponentView(uZ),t.registerPreprocessor(function(t){kX(t.series,"markArea")&&(t.markArea=t.markArea||{})})}),gb(function(t){gb(bZ),gb(DZ)}),gb(function(t){gb(VZ),gb(jZ)}),gb(VZ),gb(jZ),gb(function(t){gb(Dj),gb(Oj)}),gb(Dj),gb(Oj),gb(function(t){t.registerComponentModel(Nj),t.registerComponentView(Ej)}),gb(function(t){t.registerPreprocessor(Wj),t.registerVisual(t.PRIORITY.VISUAL.ARIA,Hj)}),gb(function(t){t.registerTransform(eq),t.registerTransform(nq)}),gb(function(t){t.registerComponentModel(iq),t.registerComponentView(rq)}),gb(function(t){t.registerUpdateLifecycle("series:beforeupdate",function(t,e,n){B(_a(n.seriesTransition),function(t){B(_a(t.to),function(t){for(var e=n.updatedSeries,i=0;i<e.length;i++)(null!=t.seriesIndex&&t.seriesIndex===e[i].seriesIndex||null!=t.seriesId&&t.seriesId===e[i].id)&&(e[i][Fy]=!0)})})}),t.registerUpdateLifecycle("series:transition",function(t,e,n){var i=Vq(e);if(i.oldSeries&&n.updatedSeries&&n.optionChanged){var r=n.seriesTransition;if(r)B(_a(r),function(t){!function(t,e,n,i){var r=[],o=[];B(_a(t.from),function(t){var n=Kq(e.oldSeries,t);n>=0&&r.push({dataGroupId:e.oldDataGroupIds[n],data:e.oldData[n],divide:qq(e.oldData[n]),groupIdDim:t.dimension})}),B(_a(t.to),function(t){var i=Kq(n.updatedSeries,t);if(i>=0){var r=n.updatedSeries[i].getData();o.push({dataGroupId:e.oldDataGroupIds[i],data:r,divide:qq(r),groupIdDim:t.dimension})}}),r.length>0&&o.length>0&&Xq(r,o,i)}(t,i,n,e)});else{var o=function(t,e){var n=mt(),i=mt(),r=mt();return B(t.oldSeries,function(e,n){var o=t.oldDataGroupIds[n],a=t.oldData[n],s=Zq(e),l=jq(s);i.set(l,{dataGroupId:o,data:a}),X(s)&&B(s,function(t){r.set(t,{key:l,dataGroupId:o,data:a})})}),B(e.updatedSeries,function(t){if(t.isUniversalTransitionEnabled()&&t.isAnimationEnabled()){var e=t.get("dataGroupId"),o=t.getData(),a=Zq(t),s=jq(a),l=i.get(s);if(l)n.set(s,{oldSeries:[{dataGroupId:l.dataGroupId,divide:qq(l.data),data:l.data}],newSeries:[{dataGroupId:e,divide:qq(o),data:o}]});else if(X(a)){var u=[];B(a,function(t){var e=i.get(t);e.data&&u.push({dataGroupId:e.dataGroupId,divide:qq(e.data),data:e.data})}),u.length&&n.set(s,{oldSeries:u,newSeries:[{dataGroupId:e,data:o,divide:qq(o)}]})}else{var c=r.get(a);if(c){var h=n.get(c.key);h||(h={oldSeries:[{dataGroupId:c.dataGroupId,data:c.data,divide:qq(c.data)}],newSeries:[]},n.set(c.key,h)),h.newSeries.push({dataGroupId:e,data:o,divide:qq(o)})}}}}),n}(i,n);B(o.keys(),function(t){var n=o.get(t);Xq(n.oldSeries,n.newSeries,e)})}B(n.updatedSeries,function(t){t[Fy]&&(t[Fy]=!1)})}for(var a=t.getSeries(),s=i.oldSeries=[],l=i.oldDataGroupIds=[],u=i.oldData=[],c=0;c<a.length;c++){var h=a[c].getData();h.count()<1e4&&(s.push(a[c]),l.push(a[c].get("dataGroupId")),u.push(h))}})}),gb(function(t){t.registerUpdateLifecycle("series:beforeupdate",function(t,e,n){var i=vI(e).labelManager;i||(i=vI(e).labelManager=new gI),i.clearLabels()}),t.registerUpdateLifecycle("series:layoutlabels",function(t,e,n){var i=vI(e).labelManager;B(n.updatedSeries,function(t){i.addLabelsOfSeries(e.getViewOfSeriesModel(t))}),i.updateLayoutConfig(e),i.layout(e),i.processLabelsOverall()})}),gb(function(t){!function(t){function e(t,e){var n=[],i=Ra(e,t);function r(e,r){B(i[e],function(e){B(e.updateAxisBreaks(t).breaks,function(t){var i;n.push(P(((i={})[r]=e.componentIndex,i),t))})})}return r("xAxisModels","xAxisIndex"),r("yAxisModels","yAxisIndex"),r("singleAxisModels","singleAxisIndex"),{eventBreaks:n}}t.registerAction($C,e),t.registerAction(JC,e),t.registerAction(QC,e)}(t),dK(),mK()}),gb(function(){ok=xK}),gb(function(t){t.registerLayout(t.PRIORITY.VISUAL.POST_CHART_LAYOUT,Fk())}),t.Axis=SM,t.ChartView=tm,t.ComponentModel=lg,t.ComponentView=Ky,t.List=Hb,t.Model=kp,t.PRIORITY=t_,t.SeriesModel=Hy,t.color=Di,t.connect=function(t){if(X(t)){var e=t;t=null,B(e,function(e){null!=e.group&&(t=e.group)}),t=t||"g_"+X_++,B(e,function(e){e.group=t})}return U_[t]=!0,t},t.dataTool={},t.dependencies={zrender:"6.1.0"},t.disConnect=q_,t.disconnect=j_,t.dispose=function(t){j(t)?t=W_[t]:t instanceof L_||(t=K_(t)),t instanceof L_&&!t.isDisposed()&&t.dispose()},t.env=r,t.extendChartView=function(t){var e=tm.extend(t);return tm.registerClass(e),e},t.extendComponentModel=function(t){var e=lg.extend(t);return lg.registerClass(e),e},t.extendComponentView=function(t){var e=Ky.extend(t);return Ky.registerClass(e),e},t.extendSeriesModel=function(t){var e=Hy.extend(t);return Hy.registerClass(e),e},t.format=nM,t.getCoordinateSystemDimensions=function(t){var e=Vf.get(t);if(e)return e.getDimensionsInfo?e.getDimensionsInfo():e.dimensions.slice()},t.getInstanceByDom=K_,t.getInstanceById=function(t){return W_[t]},t.getMap=function(t){var e=nx("getMap");return e&&e(t)},t.graphic=eM,t.helper=VS,t.init=function(t,e,n){var i=!(n&&n.ssr);if(i){0;var r=K_(t);if(r)return r;0}var o=new L_(t,e,n);return o.id="ec_"+Y_++,W_[o.id]=o,i&&Ga(t,Z_,o.id),I_(o),tx.trigger("afterinit",o),o},t.innerDrawElementOnCanvas=Fx,t.matrix=Le,t.number=QS,t.parseGeoJSON=JS,t.parseGeoJson=JS,t.registerAction=ib,t.registerCoordinateSystem=rb,t.registerCustomSeries=function(t,e){rx(t,e)},t.registerLayout=ob,t.registerLoading=ub,t.registerLocale=Gp,t.registerMap=cb,t.registerPostInit=tb,t.registerPostUpdate=eb,t.registerPreprocessor=J_,t.registerProcessor=Q_,t.registerTheme=$_,t.registerTransform=hb,t.registerUpdateLifecycle=nb,t.registerVisual=ab,t.setCanvasCreator=function(t){h({createCanvas:t})},t.setPlatformAPI=h,t.throttle=um,t.time=tM,t.use=gb,t.util=iM,t.vector=jt,t.version="6.1.0",t.zrUtil=Tt,t.zrender=Io,Object.defineProperty(t,"__esModule",{value:!0})});


Output:
Chunk ID: 02c794
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 0
Output:


Input:
*** Begin Patch
*** Update File: scripts/health/dashboard_v5/read_api.py
@@
                AND histamine_unknown_count=0 AND histamine_score IS NOT NULL
-               AND histamine_label IN ('green','yellow','orange','red')"""
+               AND histamine_label IN ('classified','green','yellow','orange','red')"""
*** End Patch


Success. Updated the following files:
M scripts/health/dashboard_v5/read_api.py

@@ -433,3 +433,3 @@
                AND histamine_unknown_count=0 AND histamine_score IS NOT NULL
-               AND histamine_label IN ('green','yellow','orange','red')"""
+               AND histamine_label IN ('classified','green','yellow','orange','red')"""
     parameters: list[Any] = []


Output:
Exit code: 0
Wall time: 0.1 seconds
Output:
Success. Updated the following files:
M scripts/health/dashboard_v5/read_api.py


Input:
*** Begin Patch
*** Update File: tests/test_dashboard_v5_sprint6g_nutrition.py
@@
-import urllib.parse
*** End Patch


@@ -5,3 +5,2 @@
 import importlib.util
-import urllib.parse
 from datetime import date


Output:
Exit code: 0
Wall time: 0.1 seconds
Output:
Success. Updated the following files:
M tests/test_dashboard_v5_sprint6g_nutrition.py


The focused Python files compile. I’m running the Sprint 6G nutrition tests now; if those pass, I’ll run the nearest V5 contract tests that exercise bundle validation and rendering.


Output:
Chunk ID: 1396e7
Wall time: 8.7817 seconds
Process exited with code 1
Original token count: 367
Output:
....F                                                                    [100%]
=================================== FAILURES ===================================
________ test_mapping_submission_requires_one_time_csrf_and_exact_shape ________

monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x746b072ea1d0>

    def test_mapping_submission_requires_one_time_csrf_and_exact_shape(monkeypatch: pytest.MonkeyPatch) -> None:
>       import health_dashboard_server as server

tests/test_dashboard_v5_sprint6g_nutrition.py:181:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
scripts/health/health_dashboard_server.py:38: in <module>
    DASHBOARD_FILE = Path(os.environ["HEALTH_DASHBOARD_FILE"]).resolve()
                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = environ({'SHELL': '/bin/bash', 'HERMES_MEDIA_TRUST_RECENT_FILES': '1', 'DISCORD_REACTIONS': 'true', 'QT_ACCESSIBILITY'...tests/test_dashboard_v5_sprint6g_nutrition.py::test_mapping_submission_requires_one_time_csrf_and_exact_shape (call)'})
key = 'HEALTH_DASHBOARD_FILE'

>   ???
E   KeyError: 'HEALTH_DASHBOARD_FILE'

<frozen os>:679: KeyError
=========================== short test summary info ============================
FAILED tests/test_dashboard_v5_sprint6g_nutrition.py::test_mapping_submission_requires_one_time_csrf_and_exact_shape
1 failed, 4 passed in 8.58s


The first focused run has four passes and one import-time environment failure in the new server validation test. I’m fixing the test setup to provide the server’s required local file env vars before import.


Input:
*** Begin Patch
*** Update File: tests/test_dashboard_v5_sprint6g_nutrition.py
@@
 def test_mapping_submission_requires_one_time_csrf_and_exact_shape(monkeypatch: pytest.MonkeyPatch) -> None:
+    monkeypatch.setenv("HEALTH_DASHBOARD_FILE", "/tmp/synthetic-v4.html")
     import health_dashboard_server as server
*** End Patch


@@ -180,2 +180,3 @@
 def test_mapping_submission_requires_one_time_csrf_and_exact_shape(monkeypatch: pytest.MonkeyPatch) -> None:
+    monkeypatch.setenv("HEALTH_DASHBOARD_FILE", "/tmp/synthetic-v4.html")
     import health_dashboard_server as server


Output:
Exit code: 0
Wall time: 0.1 seconds
Output:
Success. Updated the following files:
M tests/test_dashboard_v5_sprint6g_nutrition.py


The environment setup is fixed. I’m rerunning the focused tests and then I’ll address any contract drift from the broader V5 tests.


Output:
Chunk ID: 211fa2
Wall time: 8.7228 seconds
Process exited with code 0
Original token count: 25
Output:
.....                                                                    [100%]
5 passed in 8.52s


Sprint 6G nutrition tests now pass. I’m running the core V5 contract tests next because the bundle validation and rendering rules are the most likely places for this shape change to surface.


pytest -q tests/test_health_dashboard_v5.py tests/test_dashboard_v5_sprint6b_api.py tests/test_dashboard_v5_sprint6c_api_contracts.py


Output:
Chunk ID: 4ffe45
Wall time: 0.2612 seconds
Process exited with code 2
Original token count: 282
Output:

==================================== ERRORS ====================================
___________ ERROR collecting tests/test_dashboard_v5_sprint6b_api.py ___________
ImportError while importing test module '/home/agent/.hermes/repos/HealthManager/tests/test_dashboard_v5_sprint6b_api.py'.
Hint: make sure your test modules/packages have valid Python names.
Traceback:
../../../.local/share/uv/python/cpython-3.11.15-linux-x86_64-gnu/lib/python3.11/importlib/__init__.py:126: in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_dashboard_v5_sprint6b_api.py:33: in <module>
    from tests.fixtures.dashboard_v5_fixture import build_dashboard_v5_fixture  # noqa: E402
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E   ModuleNotFoundError: No module named 'tests.fixtures'
=========================== short test summary info ============================
ERROR tests/test_dashboard_v5_sprint6b_api.py
!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!
1 error in 0.14s


The broader API test command hit an existing collection import issue for `tests.fixtures` under this runner. I’m rerunning with `PYTHONPATH=.` so those tests can import the fixture package as intended.


PYTHONPATH=. pytest -q tests/test_health_dashboard_v5.py tests/test_dashboard_v5_sprint6b_api.py tests/test_dashboard_v5_sprint6c_api_contracts.py


Output:
Chunk ID: a79b3f
Wall time: 30.0010 seconds
Process running with session ID 98746
Original token count: 8
Output:
.................F.............


The broader V5 run is still active and has one failure so far. I’m waiting for the full failure report before patching so I don’t chase partial output.


Output:
Chunk ID: 1fded5
Wall time: 7.1617 seconds
Process exited with code 1
Original token count: 2070
Output:
FF.....                                   [100%]
=================================== FAILURES ===================================
___________ test_server_exposes_v5_preview_and_exact_asset_allowlist ___________

tmp_path = PosixPath('/tmp/pytest-of-agent/pytest-734/test_server_exposes_v5_preview0')
monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x7b67771d3090>

    def test_server_exposes_v5_preview_and_exact_asset_allowlist(tmp_path: Path, monkeypatch) -> None:
        v4 = tmp_path / "v4.html"
        v5 = tmp_path / "v5.html"
        v4.write_text("v4", encoding="utf-8")
        v5.write_text("", encoding="utf-8")
        assets = tmp_path / "assets"
        assets.mkdir()
        for name in ("chart.umd.min.js", "dashboard-v5.js", "dashboard-v5.css"):
            (assets / name).write_text("/* synthetic asset */", encoding="utf-8")
        monkeypatch.setenv("HEALTH_DASHBOARD_FILE", str(v4))
        monkeypatch.setenv("HEALTH_DASHBOARD_V5_FILE", str(v5))
        spec = importlib.util.spec_from_file_location(
            "health_dashboard_server_v5_test", HEALTH / "health_dashboard_server.py"
        )
        assert spec and spec.loader
        server_module = importlib.util.module_from_spec(spec)
        spec.loader.exec_module(server_module)
        monkeypatch.setattr(server_module, "ASSET_DIR", assets)
>       server = server_module.ThreadingHTTPServer(("127.0.0.1", 0), server_module.Handler)
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

tests/test_health_dashboard_v5.py:420:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../../.local/share/uv/python/cpython-3.11.15-linux-x86_64-gnu/lib/python3.11/socketserver.py:452: in __init__
    self.socket = socket.socket(self.address_family,
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <socket.socket fd=-1, family=0, type=0, proto=0>
family = <AddressFamily.AF_INET: 2>, type = <SocketKind.SOCK_STREAM: 1>
proto = 0, fileno = None

    def __init__(self, family=-1, type=-1, proto=-1, fileno=None):
        # For user code address family and type values are IntEnum members, but
        # for the underlying _socket.socket they're just integers. The
        # constructor of _socket.socket converts the given argument to an
        # integer automatically.
        if fileno is None:
            if family == -1:
                family = AF_INET
            if type == -1:
                type = SOCK_STREAM
            if proto == -1:
                proto = 0
>       _socket.socket.__init__(self, family, type, proto, fileno)
E       PermissionError: [Errno 1] Operation not permitted

../../../.local/share/uv/python/cpython-3.11.15-linux-x86_64-gnu/lib/python3.11/socket.py:232: PermissionError
_________ test_http_api_requires_private_bearer_token_and_never_caches _________

tmp_path = PosixPath('/tmp/pytest-of-agent/pytest-734/test_http_api_requires_private0')
monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x7b6777205250>

    def test_http_api_requires_private_bearer_token_and_never_caches(tmp_path, monkeypatch):
        database = api_database(tmp_path)
        dashboard = tmp_path / "dashboard.html"
        dashboard.write_text("", encoding="utf-8")
        token = "synthetic_api_token_0123456789abcdef"
        token_file = tmp_path / "api-token"
        token_file.write_text(token, encoding="ascii")
        token_file.chmod(0o600)
        module = load_server("health_dashboard_server_sprint6b", dashboard, database, token_file, monkeypatch)
        assert module.API_DB == database.resolve()
        assert module.DB != database.resolve()
>       server = module.ThreadingHTTPServer(("127.0.0.1", 0), module.Handler)
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

tests/test_dashboard_v5_sprint6b_api.py:583:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../../.local/share/uv/python/cpython-3.11.15-linux-x86_64-gnu/lib/python3.11/socketserver.py:452: in __init__
    self.socket = socket.socket(self.address_family,
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <socket.socket fd=-1, family=0, type=0, proto=0>
family = <AddressFamily.AF_INET: 2>, type = <SocketKind.SOCK_STREAM: 1>
proto = 0, fileno = None

    def __init__(self, family=-1, type=-1, proto=-1, fileno=None):
        # For user code address family and type values are IntEnum members, but
        # for the underlying _socket.socket they're just integers. The
        # constructor of _socket.socket converts the given argument to an
        # integer automatically.
        if fileno is None:
            if family == -1:
                family = AF_INET
            if type == -1:
                type = SOCK_STREAM
            if proto == -1:
                proto = 0
>       _socket.socket.__init__(self, family, type, proto, fileno)
E       PermissionError: [Errno 1] Operation not permitted

../../../.local/share/uv/python/cpython-3.11.15-linux-x86_64-gnu/lib/python3.11/socket.py:232: PermissionError
____ test_http_api_is_fail_closed_without_explicit_database_and_token_paths ____

tmp_path = PosixPath('/tmp/pytest-of-agent/pytest-734/test_http_api_is_fail_closed_w0')
monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x7b67789120d0>

    def test_http_api_is_fail_closed_without_explicit_database_and_token_paths(tmp_path, monkeypatch):
        dashboard = tmp_path / "dashboard.html"
        dashboard.write_text("", encoding="utf-8")
        monkeypatch.delenv("HEALTH_DASHBOARD_TEST_INSTANCE_ID", raising=False)
        monkeypatch.delenv("HEALTH_DASHBOARD_ACTION_INBOX", raising=False)
        monkeypatch.setenv("HEALTH_DASHBOARD_FILE", str(dashboard))
        monkeypatch.setenv("HEALTH_DASHBOARD_V5_FILE", str(dashboard))
        monkeypatch.delenv("HEALTH_DASHBOARD_DB", raising=False)
        monkeypatch.delenv("HEALTH_DASHBOARD_API_TOKEN_FILE", raising=False)
        spec = importlib.util.spec_from_file_location("health_dashboard_server_sprint6b_disabled", SERVER)
        assert spec and spec.loader
        module = importlib.util.module_from_spec(spec)
        sys.modules[spec.name] = module
        spec.loader.exec_module(module)
>       server = module.ThreadingHTTPServer(("127.0.0.1", 0), module.Handler)
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

tests/test_dashboard_v5_sprint6b_api.py:763:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../../.local/share/uv/python/cpython-3.11.15-linux-x86_64-gnu/lib/python3.11/socketserver.py:452: in __init__
    self.socket = socket.socket(self.address_family,
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <socket.socket fd=-1, family=0, type=0, proto=0>
family = <AddressFamily.AF_INET: 2>, type = <SocketKind.SOCK_STREAM: 1>
proto = 0, fileno = None

    def __init__(self, family=-1, type=-1, proto=-1, fileno=None):
        # For user code address family and type values are IntEnum members, but
        # for the underlying _socket.socket they're just integers. The
        # constructor of _socket.socket converts the given argument to an
        # integer automatically.
        if fileno is None:
            if family == -1:
                family = AF_INET
            if type == -1:
                type = SOCK_STREAM
            if proto == -1:
                proto = 0
>       _socket.socket.__init__(self, family, type, proto, fileno)
E       PermissionError: [Errno 1] Operation not permitted

../../../.local/share/uv/python/cpython-3.11.15-linux-x86_64-gnu/lib/python3.11/socket.py:232: PermissionError
=========================== short test summary info ============================
FAILED tests/test_health_dashboard_v5.py::test_server_exposes_v5_preview_and_exact_asset_allowlist
FAILED tests/test_dashboard_v5_sprint6b_api.py::test_http_api_requires_private_bearer_token_and_never_caches
FAILED tests/test_dashboard_v5_sprint6b_api.py::test_http_api_is_fail_closed_without_explicit_database_and_token_paths
3 failed, 35 passed in 40.53s


The broader V5 run passed all non-socket checks; the three failures are the sandbox denying local socket creation, not assertion failures in the changed code. I’m updating ADR 0003 now with the required inventory/methodology/conclusion details.


Input:
*** Begin Patch
*** Update File: docs/adr/0003-dietary-inflammation-model.md
@@
-## Context
-
-Nutrition data combines imported meal records, nutrient totals, SIGHi-style histamine mappings, symptom dimensions, and optional context such as medication phases or documented events. These inputs are incomplete on many days and may contain unknown food mappings.
+## Context
+
+Nutrition data combines imported meal records, nutrient totals, SIGHi-style histamine mappings, symptom dimensions, and optional context such as medication phases or documented events. These inputs are incomplete on many days and may contain unknown food mappings.
+
+Technical inventory from the Sprint 6G-A fixture/audit, abstracted without product names:
+
+- nutrition item records: present, with meal, date, item label, imported macros, and source-local product identifiers;
+- item nutrient rows: present for selected macro and nutrient keys, with mixed unit coverage;
+- histamine mapping rows: present for mapped and unmapped item states;
+- review queue rows: present for unmapped normalized product slugs;
+- daily and meal summaries: present, but historically allowed zero defaults for missing macro/index fields;
+- DII reference-data tables: absent;
+- DII food-parameter normalization/version tables: absent;
+- reference licensing evidence for the full DII parameter database: absent.
+
+Input coverage/unit findings:
+
+- known and displayable nutrient units are limited to kcal, g, and mg for allowlisted keys;
+- unknown-unit micronutrients are omitted from read APIs;
+- missing imported macros are unknown and must not be normalized to zero;
+- currently displayable inflammation-adjacent components are fiber, sugar, saturated fat, and sodium where known;
+- required DII parameters are not covered with a complete, licensed, versioned reference table.
+
+The Dietary Inflammatory Index methodology required for a real DII implementation is the peer-reviewed Shivappa et al. 2014 method: nutrient/food-parameter intake is standardized against a global comparative database, converted to centered percentiles, multiplied by literature-derived inflammatory effect scores, and summed across available parameters. That methodology requires stable parameter names, units, global means/standard deviations, effect scores, coverage thresholds, and versioning. Those inputs are not available in this project state.
@@
 Dashboard v5 exposes transparent components only:

 - documented macros and allowlisted nutrients,
 - versioned histamine mapping coverage,
 - unknown item counts,
 - explicitly complete or incomplete day semantics,
 - side-by-side ECharts views for nutrition and Aphthae when both are documented.

 The dashboard does not compute a dietary inflammation index when required inputs are insufficient. Unknown or unmapped foods make the histamine component incomplete rather than zero. Composite products require ingredient review before a definitive mapping is applied.

 Personal tolerance labels remain separate from shared food mapping definitions.
+
+Missing-parameter handling:
+
+- unobserved DII parameters are not imputed as neutral;
+- known values are shown only as individual documented components;
+- any future DII calculation must store the methodology id, source version, parameter coverage, unit normalization map, and unavailable-parameter policy with each computed value.
+
+Conclusion: Entzündungsindex derzeit nicht belastbar berechenbar.
*** End Patch


Success. Updated the following files:
M docs/adr/0003-dietary-inflammation-model.md

@@ -12,2 +12,23 @@

+Technical inventory from the Sprint 6G-A fixture/audit, abstracted without product names:
+
+- nutrition item records: present, with meal, date, item label, imported macros, and source-local product identifiers;
+- item nutrient rows: present for selected macro and nutrient keys, with mixed unit coverage;
+- histamine mapping rows: present for mapped and unmapped item states;
+- review queue rows: present for unmapped normalized product slugs;
+- daily and meal summaries: present, but historically allowed zero defaults for missing macro/index fields;
+- DII reference-data tables: absent;
+- DII food-parameter normalization/version tables: absent;
+- reference licensing evidence for the full DII parameter database: absent.
+
+Input coverage/unit findings:
+
+- known and displayable nutrient units are limited to kcal, g, and mg for allowlisted keys;
+- unknown-unit micronutrients are omitted from read APIs;
+- missing imported macros are unknown and must not be normalized to zero;
+- currently displayable inflammation-adjacent components are fiber, sugar, saturated fat, and sodium where known;
+- required DII parameters are not covered with a complete, licensed, versioned reference table.
+
+The Dietary Inflammatory Index methodology required for a real DII implementation is the peer-reviewed Shivappa et al. 2014 method: nutrient/food-parameter intake is standardized against a global comparative database, converted to centered percentiles, multiplied by literature-derived inflammatory effect scores, and summed across available parameters. That methodology requires stable parameter names, units, global means/standard deviations, effect scores, coverage thresholds, and versioning. Those inputs are not available in this project state.
+
 ## Decision
@@ -26,2 +47,10 @@

+Missing-parameter handling:
+
+- unobserved DII parameters are not imputed as neutral;
+- known values are shown only as individual documented components;
+- any future DII calculation must store the methodology id, source version, parameter coverage, unit normalization map, and unavailable-parameter policy with each computed value.
+
+Conclusion: Entzündungsindex derzeit nicht belastbar berechenbar.
+
 ## Consequences


Output:
Exit code: 0
Wall time: 0.1 seconds
Output:
Success. Updated the following files:
M docs/adr/0003-dietary-inflammation-model.md


ADR 0003 now contains the required methodology and limitation language, including the explicit “not reliably computable” conclusion. I’m adding one more focused bootstrap/idempotency test for schema migration gates and an affected-day-only assertion.


Input:
*** Begin Patch
*** Update File: tests/test_dashboard_v5_sprint6g_nutrition.py
@@
 def test_mapping_worker_is_transactional_idempotent_and_recomputes_only_affected_days(
     fixture_db: Path, monkeypatch: pytest.MonkeyPatch
 ) -> None:
@@
     monkeypatch.setattr(worker, "DASHBOARD_DB", fixture_db)
     monkeypatch.setattr(worker, "DASHBOARD_V5_FILE", None)
+    connection = sqlite3.connect(fixture_db)
+    try:
+        connection.execute(
+            """INSERT INTO nutrition_daily_summary_v2
+               (datum,kcal,protein_g,carb_g,fat_g,item_count,histamine_score,histamine_max,histamine_unknown_count,histamine_label,nutrient_json)
+               VALUES('2026-06-13',123,4,5,6,1,2,2,0,'classified','{}')"""
+        )
+        connection.commit()
+    finally:
+        connection.close()
@@
         assert connection.execute("SELECT COUNT(*) FROM nutrition_mapping_action_log").fetchone()[0] == 1
         assert connection.execute("SELECT COUNT(*) FROM nutrition_mapping_provenance WHERE affected_day='2026-06-14'").fetchone()[0] == 1
+        unaffected = connection.execute("SELECT kcal,histamine_score FROM nutrition_daily_summary_v2 WHERE datum='2026-06-13'").fetchone()
+        assert dict(unaffected) == {"kcal": 123.0, "histamine_score": 2.0}
@@
     finally:
         connection.close()
+
+
+def test_worker_schema_bootstrap_is_idempotent_and_preserves_raw_items(fixture_db: Path) -> None:
+    import health_dashboard_action_worker as worker
+
+    connection = sqlite3.connect(fixture_db)
+    connection.row_factory = sqlite3.Row
+    try:
+        before = connection.execute(
+            "SELECT COUNT(*),SUM(raw_item_json IS NOT NULL),SUM(raw_product_json IS NOT NULL) FROM nutrition_items"
+        ).fetchone()
+        worker.ensure_nutrition_mapping_schema(connection)
+        worker.ensure_nutrition_mapping_schema(connection)
+        after = connection.execute(
+            "SELECT COUNT(*),SUM(raw_item_json IS NOT NULL),SUM(raw_product_json IS NOT NULL) FROM nutrition_items"
+        ).fetchone()
+        assert tuple(before) == tuple(after)
+        columns = {row["name"] for row in connection.execute("PRAGMA table_info(nutrition_mapping_provenance)")}
+        assert {"decision", "alias", "mapping_method", "source_label", "source_version", "definition_version"} <= columns
+        assert connection.execute("PRAGMA integrity_check").fetchone()[0] == "ok"
+    finally:
+        connection.close()
*** End Patch