Output:
Chunk ID: 1ef29d
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1764
Output:
        "timezone": TZ_NAME,
        "is_today": requested == today,
        "is_future": requested > today,
        "coverage": {
            "status": "documented" if categories else "not_documented",
            "categories": categories,
            "counts": counts,
            "symptoms_complete": symptoms["complete"],
        },
        "measurements": measurements,
        "metrics": metrics,
        "symptoms": symptoms,
        "medications": medications,
        "events": day_events,
        "labs": labs,
        "nutrition": nutrition,
        "documents": documents,
        "appointments": appointments,
        "notices": notices[:3],
    }

def _calendar(connection: sqlite3.Connection, params: dict[str, str]) -> dict[str, Any]:
    raw_start = params.get("from")
    raw_end = params.get("to")
    if not raw_start or not raw_end:
        raise APIError(400, "range_required")
    start = parse_iso_day(raw_start)
    end = parse_iso_day(raw_end)
    if end < start:
        raise APIError(400, "range_reversed")
    days = (end - start).days + 1
    if days > MAX_CALENDAR_DAYS:
        raise APIError(422, "calendar_range_too_large")
    today = local_today()
    if end > today + timedelta(days=MAX_FUTURE_DAYS):
        raise APIError(422, "future_range_too_large")
    dates = [(start + timedelta(days=offset)).isoformat() for offset in range(days)]
    counts_by_day = {
        day: {
            "measurements": 0,
            "symptoms": 0,
            "medications": 0,
            "labs": 0,
            "events": 0,
            "documents": 0,
            "nutrition": 0,
            "appointments": 0,
        }
        for day in dates
    }

    def grouped_count(
        table: str, date_column: str, category: str, where: str = "1=1"
    ) -> None:
        if not table_exists(connection, table):
            return
        sql = (
            f"SELECT substr({date_column},1,10) AS day,COUNT(*) AS count FROM {table} "
            f"WHERE {where} AND substr({date_column},1,10) BETWEEN ? AND ? GROUP BY substr({date_column},1,10)"
        )
        for row in connection.execute(sql, (start.isoformat(), end.isoformat())):
            if row["day"] in counts_by_day:
                counts_by_day[row["day"]][category] += min(int(row["count"]), 999)

    # Apple Health uses the same allowlisted metric paths and canonical Zurich-day
    # normalization as /api/v1/day.  A row is visible only if its source identifier
    # is released in APPLE_SOURCE_SPECS; unrecognized Apple rows never influence
    # calendar categories or counts.
    apple_metrics: dict[str, MetricV2] = {}
    for metric in BY_ID_V2.values():
        if (
            metric.source == "apple_health"
            and metric.value_type != "panel"
            and metric.source_identifier in APPLE_SOURCE_SPECS
        ):
            apple_metrics.setdefault(metric.source_identifier, metric)
    for metric in apple_metrics.values():
        for point in _metric_points(connection, metric, start, end):
            observed_day = point["date"]
            if observed_day in counts_by_day:
                counts_by_day[observed_day]["measurements"] = min(
                    counts_by_day[observed_day]["measurements"] + 1, 999
                )

    grouped_count("vitalzeichen", "datum", "measurements")
    grouped_count("symptom_log", "datum", "symptoms", "kontext='daily_quick_score'")
    grouped_count(
        "laborwerte",
        "abnahme_datum",
        "labs",
        "validierungsstatus='validiert' AND verified_against_original=1",
    )
    grouped_count("health_events", "date", "events")
    grouped_count("dokumente", "document_date", "documents", "review_status='geprueft'")
    grouped_count("nutrition_daily_summary_v2", "datum", "nutrition")
    grouped_count("arztbesuche", "datum", "appointments")

    if table_exists(connection, "medication_administrations"):
        seen_medications: dict[str, set[int]] = {day: set() for day in dates}
        for row in connection.execute(
            """SELECT id,datum,scheduled_next_date FROM medication_administrations
               WHERE datum BETWEEN ? AND ? OR scheduled_next_date BETWEEN ? AND ? LIMIT 10000""",
            (start.isoformat(), end.isoformat(), start.isoformat(), end.isoformat()),
        ):
            for candidate in (
                parse_day(row["datum"]),
                parse_day(row["scheduled_next_date"]),
            ):
                if candidate in seen_medications:
                    seen_medications[candidate].add(int(row["id"]))
        for day, identifiers in seen_medications.items():
            counts_by_day[day]["medications"] = min(len(identifiers), 999)

    if table_exists(connection, "health_event_periods"):
        for row in connection.execute(
            """SELECT start_date,end_date FROM health_event_periods
               WHERE start_date<=? AND COALESCE(end_date,start_date)>=? LIMIT 10000""",
            (end.isoformat(), start.isoformat()),
        ):
            period_start = parse_day(row["start_date"])
            period_end = parse_day(row["end_date"]) if row["end_date"] else period_start
            if not period_start or not period_end:
                continue
            current = max(period_start, start.isoformat())
            bounded_end = min(period_end, end.isoformat())
            while current <= bounded_end:
                counts_by_day[current]["events"] = min(
                    counts_by_day[current]["events"] + 1, 999
                )
                current = (date.fromisoformat(current) + timedelta(days=1)).isoformat()

    result = []
    for current in dates:
        counts = counts_by_day[current]
        categories = [name for name, count in counts.items() if count]
        result.append(
            {
                "date": current,
                "is_today": current == today.isoformat(),
                "categories": categories,
                "counts": counts,
                "completeness": "documented" if categories else "not_documented",
                "codes": [f"has_{category}" for category in categories],
            }
        )
    return {
        "from": start.isoformat(),
        "to": end.isoformat(),
        "today": today.isoformat(),
        "timezone": TZ_NAME,
        "days": result,
    }

RECORD_TABS = frozenset(
    {"overview", "labs", "medications", "appointments", "documents", "report"}
)
RECORD_DOCUMENT_SORTS = {
    "document_date_desc": "document_date DESC,id DESC",
    "document_date_asc": "document_date ASC,id ASC",
    "import_date_desc": "upload_datum DESC,id DESC",
    "category": "kategorie COLLATE NOCASE,id DESC",
    "institution": "institution COLLATE NOCASE,id DESC",
    "type": "daten_typ,id DESC",
    "review_status": "review_status,id DESC",
}
RECORD_MAX_ROWS = 100
RECORD_PAGE_SIZE = 25
RECORD_MAX_PAGE_SIZE = 50
RECORD_MAX_MATCHES = 50
RECORD_MAX_SECTIONS = 20
RECORD_MAX_SNIPPET_CHARS = 320
_RECORD_CURSOR_KEY = secrets.token_bytes(32)
_RECORD_CURSOR_VERSION = 1
_RECORD_SORT_SPECS = {


Output:
Chunk ID: 07ca9e
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 4920
Output:
    if (
        supplied.scheme != "http"
        or not supplied.hostname
        or supplied.username is not None
        or supplied.password is not None
        or supplied.path
        or supplied.params
        or supplied.query
        or supplied.fragment
    ):
        return False
    request_host = request_authority.hostname
    return (
        request_host is not None
        and supplied.hostname.lower().rstrip(".") == request_host.lower().rstrip(".")
        and supplied_port == request_port
    )

def issue_csrf_token(now: float | None = None) -> str:
    issued_at = time.monotonic() if now is None else now
    token = secrets.token_urlsafe(24)
    with _csrf_lock:
        expired = [
            existing
            for existing, timestamp in _csrf_tokens.items()
            if issued_at - timestamp > CSRF_TTL_SECONDS
        ]
        for existing in expired:
            _csrf_tokens.pop(existing, None)
        _csrf_tokens[token] = issued_at
        while len(_csrf_tokens) > 256:
            oldest = min(_csrf_tokens, key=_csrf_tokens.get)  # type: ignore[arg-type]
            _csrf_tokens.pop(oldest, None)
    return token

def consume_csrf_token(token: str, now: float | None = None) -> bool:
    checked_at = time.monotonic() if now is None else now
    with _csrf_lock:
        issued_at = _csrf_tokens.pop(token, None)
    return issued_at is not None and 0 <= checked_at - issued_at <= CSRF_TTL_SECONDS

def issue_queue_receipt(now: float | None = None) -> str:
    issued_at = time.monotonic() if now is None else now
    token = secrets.token_urlsafe(18)
    with _queue_receipt_lock:
        _queue_receipts[token] = issued_at
        expired = [
            key
            for key, timestamp in _queue_receipts.items()
            if issued_at - timestamp > QUEUE_RECEIPT_TTL_SECONDS
        ]
        for key in expired:
            _queue_receipts.pop(key, None)
        while len(_queue_receipts) > 256:
            oldest = min(_queue_receipts, key=_queue_receipts.get)  # type: ignore[arg-type]
            _queue_receipts.pop(oldest, None)
    return token

def consume_queue_receipt(token: str, now: float | None = None) -> bool:
    checked_at = time.monotonic() if now is None else now
    with _queue_receipt_lock:
        issued_at = _queue_receipts.pop(token, None)
    return (
        issued_at is not None
        and 0 <= checked_at - issued_at <= QUEUE_RECEIPT_TTL_SECONDS
    )

def dashboard_html_with_nonce(
    path: Path,
    *,
    capture_queued: bool = False,
) -> tuple[bytes, str, str, str]:
    nonce = secrets.token_urlsafe(18)
    csrf_token = issue_csrf_token()
    browser_session_csrf_token = issue_csrf_token()
    text = path.read_text(encoding="utf-8")
    capture_day = local_today()
    text = text.replace("__CSRF_TOKEN__", csrf_token)
    text = text.replace("__BROWSER_SESSION_CSRF__", browser_session_csrf_token)
    text = text.replace("__CAPTURE_QUEUED__", "true" if capture_queued else "false")
    text = text.replace("__CAPTURE_DATE__", capture_day.isoformat())
    text = text.replace("__CAPTURE_DATE_LABEL__", capture_day.strftime("%d.%m.%Y"))
    text = text.replace("<script", f"<script nonce='{nonce}'")
    text = text.replace("<style", f"<style nonce='{nonce}'")
    return text.encode("utf-8"), nonce, csrf_token, browser_session_csrf_token

def local_today() -> date:
    return datetime.now(CAPTURE_TIMEZONE).date()

def validate_symptom_form(
    form: dict[str, list[str]],
) -> tuple[str, dict[str, int], str]:
    day = (form.get("date") or [""])[0]
    try:
        parsed_day = date.fromisoformat(day)
    except ValueError as exc:
        raise ValueError("invalid date") from exc
    if parsed_day.year < 2000 or parsed_day > local_today():
        raise ValueError("date outside allowed range")
    scores: dict[str, int] = {}
    for field in SYMPTOM_FIELDS:
        raw = (form.get(field) or [""])[0]
        if raw not in {"0", "1", "2", "3"}:
            raise ValueError(f"invalid or missing score: {field}")
        scores[field] = int(raw)
    notes = (form.get("notes") or [""])[0].strip()
    if len(notes) > 300:
        raise ValueError("notes too long")
    return parsed_day.isoformat(), scores, notes

def validate_symptom_submission(
    form: dict[str, list[str]],
) -> tuple[str, dict[str, int], str, str]:
    required = {"csrf_token", "date", "notes", *SYMPTOM_FIELDS}
    allowed = required | {"return_to"}
    keys = frozenset(form)
    if keys not in {frozenset(required), frozenset(allowed)}:
        raise ValueError("invalid submission shape")
    if any(len(values) != 1 for values in form.values()):
        raise ValueError("every field must have a single value")
    return_to = form.get("return_to", ["v4"])[0]
    if return_to not in {"v4", "v5"}:
        raise ValueError("invalid return target")
    day, scores, notes = validate_symptom_form(form)
    return day, scores, notes, return_to

def ensure_private_inbox() -> None:
    ACTION_INBOX.mkdir(mode=0o700, parents=True, exist_ok=True)
    metadata = ACTION_INBOX.lstat()
    if not stat.S_ISDIR(metadata.st_mode) or metadata.st_uid != os.getuid():
        raise OSError("action inbox is not a private owned directory")
    os.chmod(ACTION_INBOX, 0o700)

def write_symptom_checkin(day: str, scores: dict[str, int], notes: str) -> None:
    """Atomically enqueue a validated action; the network server never writes the DB."""
    with _action_lock:
        ensure_private_inbox()
        if sum(1 for _ in ACTION_INBOX.glob("*.json")) >= MAX_PENDING_ACTIONS:
            raise QueueFullError("private action queue is full")
        token = secrets.token_hex(16)
        temporary = ACTION_INBOX / f".{token}.tmp"
        destination = ACTION_INBOX / f"{token}.json"
        payload = json.dumps(
            {
                "version": 1,
                "action": "symptom_checkin",
                "date": day,
                "scores": scores,
                "notes": notes,
            },
            ensure_ascii=False,
            separators=(",", ":"),
        ).encode("utf-8")
        descriptor = os.open(temporary, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
        try:
            with os.fdopen(descriptor, "wb") as handle:
                handle.write(payload)
                handle.flush()
                os.fsync(handle.fileno())
            temporary.replace(destination)
            directory = os.open(ACTION_INBOX, os.O_RDONLY | os.O_DIRECTORY)
            try:
                os.fsync(directory)
            finally:
                os.close(directory)
        except Exception:
            temporary.unlink(missing_ok=True)
            raise

class Handler(BaseHTTPRequestHandler):
    def send_error(
        self,
        code: int,
        message: str | None = None,
        explain: str | None = None,
    ) -> None:
        request_path = urlparse(getattr(self, "path", "")).path
        if request_path.startswith("/api/v1/"):
            send_body = getattr(self, "command", "") != "HEAD"
            if not host_is_allowed(self.headers.get("Host", "")):
                self._send_api_error(421, "invalid_host", send_body)
                return
            status = 405 if code == 501 else code
            error_code = "read_only_endpoint" if code == 501 else "request_rejected"
            self._send_api_error(status, error_code, send_body)
            return
        super().send_error(code, message, explain)

    def _reject_unsupported(self) -> None:
        if not host_is_allowed(self.headers.get("Host", "")):
        if not host_is_allowed(self.headers.get("Host", "")):
            if urlparse(self.path).path.startswith("/api/v1/"):
                self._send_api_error(421, "host_not_allowed", self.command != "HEAD")
            else:
                self.send_error(421)
            return
        if urlparse(self.path).path.startswith("/api/v1/"):
            self._send_api_error(405, "read_only_endpoint", self.command != "HEAD")
            return
        self.send_error(405)

    do_OPTIONS = _reject_unsupported  # noqa: N815
    do_PUT = _reject_unsupported  # noqa: N815
    do_DELETE = _reject_unsupported  # noqa: N815
    do_PATCH = _reject_unsupported  # noqa: N815
    do_TRACE = _reject_unsupported  # noqa: N815
    do_CONNECT = _reject_unsupported  # noqa: N815

    def do_HEAD(self) -> None:  # noqa: N802
        if not host_is_allowed(self.headers.get("Host", "")):
            if urlparse(self.path).path.startswith("/api/v1/"):
                self._send_api_error(421, "host_not_allowed", False)
            else:
                self.send_error(421)
            return
        self._handle(send_body=False)

    def do_GET(self) -> None:  # noqa: N802
        if not host_is_allowed(self.headers.get("Host", "")):
            if urlparse(self.path).path.startswith("/api/v1/"):
                self._send_api_error(421, "host_not_allowed", True)
            else:
                self.send_error(421)
            return
        self._handle(send_body=True)

    def do_POST(self) -> None:  # noqa: N802
        if not host_is_allowed(self.headers.get("Host", "")):
            if urlparse(self.path).path.startswith("/api/v1/"):
                self._send_api_error(421, "host_not_allowed", True)
            else:
                self.send_error(421)
            return
        api_path = urlparse(self.path).path
        if api_path.startswith("/api/v1/"):
            if api_path == "/api/v1/browser-session":
                self._handle_browser_session(True)
            else:
                self._send_api_error(405, "read_only_endpoint", True)
            return
        if urlparse(self.path).path != CHECKIN_ROUTE:
            self.send_error(404)
            return
        origin = self.headers.get("Origin", "")
        host = self.headers.get("Host", "")
        if not origin_matches_request(origin, host):
            self.send_error(403)
            return
        if self.headers.get_content_type() != "application/x-www-form-urlencoded":
            self.send_error(415)
            return
        try:
            length = int(self.headers.get("Content-Length", "0"))
        except ValueError:
            self.send_error(400)
            return
        if length < 1 or length > 4096:
            self.send_error(413)
            return
        try:
            form = parse_qs(
                self.rfile.read(length).decode("utf-8"),
                keep_blank_values=True,
                strict_parsing=True,
            )
        except (UnicodeError, ValueError):
            self.send_error(400)
            return
        token = (form.get("csrf_token") or [""])[0]
        cookie = SimpleCookie(self.headers.get("Cookie", ""))
        cookie_token = cookie.get("health_csrf")
        if (
            not token
            or not cookie_token
            or not secrets.compare_digest(token, cookie_token.value)
        ):
            self.send_error(403)
            return
        if not consume_csrf_token(token):
            self.send_error(403)
            return
        try:
            day, scores, notes, return_to = validate_symptom_submission(form)
            write_symptom_checkin(day, scores, notes)
        except ValueError:
            self.send_error(400)
            return
        except QueueFullError:
            self.send_error(503)
            return
        except OSError:
            self.send_error(500)
            return
        if return_to == "v5":
            receipt = quote(issue_queue_receipt(), safe="")
            target = V5_ROUTE + f"?queued={receipt}#capture-status"
        else:
            target = ROUTE + "?queued=1#symptome"
        self.send_response(303)
        self.send_header("Location", target)
        self.send_header("Cache-Control", "no-store")
        self.end_headers()

    def _handle(self, send_body: bool) -> None:
        request = urlparse(self.path)
        path = request.path
        if path.startswith("/api/v1/"):
            self._handle_api(path, request.query, send_body)
            return
        if path == "/":
            self.send_response(302)
            self.send_header("Location", ROUTE)
            self.end_headers()
            return
        if path in {ROUTE, V5_ROUTE}:
            selected = DASHBOARD_FILE if path == ROUTE else DASHBOARD_V5_FILE
            if selected is None:
                self.send_error(404)
                return
            try:
                queued_values = parse_qs(request.query, keep_blank_values=True).get(
                    "queued", []
                )
                capture_queued = (
                    send_body
                    and path == V5_ROUTE
                    and len(queued_values) == 1
                    and consume_queue_receipt(queued_values[0])
                )
                data, nonce, csrf_token, browser_session_csrf_token = (
                    dashboard_html_with_nonce(
                        selected,
                        capture_queued=capture_queued,
                    )
                )
            except (OSError, UnicodeError):
                self.send_error(404)
                return
            self._send_bytes(
                data,
                selected.name,
                "text/html; charset=utf-8",
                send_body,
                nonce=nonce,
                csrf_token=csrf_token,
                browser_session_csrf_token=browser_session_csrf_token,
                connect_self=path == V5_ROUTE,
            )
            return
        if path in ASSET_ROUTES:
            filename, content_type = ASSET_ROUTES[path]
            self._serve_file(
                ASSET_DIR / filename,
                filename,
                content_type,
                send_body,
            )
            return
        if path.startswith("/health-doc/"):
            try:
                doc_id = int(path.rsplit("/", 1)[-1])
            except ValueError:
                self.send_error(404)
                return
            found = document_path(doc_id)
            if not found:
                self.send_error(404)
                return
            self._serve_file(found[0], found[1], None, send_body)
            return
        if path.startswith("/health-report/"):
            report = safe_report_path(path.rsplit("/", 1)[-1])
            if not report:
                self.send_error(404)
                return
            self._serve_file(report, report.name, None, send_body)
            return
        self.send_error(404)

    def _handle_browser_session(self, send_body: bool) -> None:
        request = urlparse(self.path)
        if request.query or API_DB is None:
            self._send_api_error(
                503 if API_DB is None else 400,
                "api_unavailable" if API_DB is None else "unknown_parameter",
                send_body,
            )
            return
        origin = self.headers.get("Origin", "")
        fetch_site = self.headers.get("Sec-Fetch-Site", "")
        if (
            origin and not origin_matches_request(origin, self.headers.get("Host", ""))
        ) or (fetch_site and fetch_site not in {"same-origin", "none"}):
            self._send_api_error(403, "origin_not_allowed", send_body)
            return
        if self.headers.get("Content-Length", "0") not in {"", "0"}:
            self._send_api_error(400, "request_rejected", send_body)
            return
        try:
            cookie = SimpleCookie(self.headers.get("Cookie", ""))
        except (CookieError, ValueError):
            self._send_api_error(403, "csrf_rejected", send_body)
            return
        csrf_header = self.headers.get("X-Health-Browser-CSRF", "")
        csrf_cookie = cookie.get("health_browser_csrf")
        if (
            not csrf_header
            or csrf_cookie is None
            or len(csrf_header) != len(csrf_cookie.value)
            or not secrets.compare_digest(csrf_header, csrf_cookie.value)
            or not consume_csrf_token(csrf_header)
        ):
            self._send_api_error(403, "csrf_rejected", send_body)
            return
        session_id = issue_browser_session()
        cookie = (
            f"health_api_session={session_id}; Path=/api/v1; HttpOnly; SameSite=Strict; "
            f"Max-Age={BROWSER_SESSION_TTL_SECONDS}"
        )
        if BROWSER_SESSION_COOKIE_SECURE:
            cookie += "; Secure"
        self.send_response(204)
        self.send_header("Set-Cookie", cookie)
        self.send_header("Cache-Control", "no-store")
        self.send_header("Pragma", "no-cache")
        self.send_header("X-Content-Type-Options", "nosniff")
        self.send_header("X-Frame-Options", "DENY")
        self.send_header("Referrer-Policy", "no-referrer")
        self.send_header(
            "Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'"
        )
        self.send_header("Content-Length", "0")
        self.end_headers()

    def _handle_api(self, path: str, query: str, send_body: bool) -> None:
        if API_DB is None:
            self._send_api_error(503, "api_unavailable", send_body)
            return
        try:
            token = load_api_token()
        except (OSError, UnicodeError):
            token = None
        bearer_authenticated = token is not None and api_request_is_authenticated(
            self.headers.get("Authorization", ""), token
        )
        session_authenticated = browser_session_is_authenticated(
            self.headers.get("Cookie", "")
        )
        if token is None:
            self._send_api_error(503, "api_unavailable", send_body)
            return
        if not bearer_authenticated and not session_authenticated:
            self._send_api_error(401, "authentication_required", send_body)
            return
        origin = self.headers.get("Origin", "")
        fetch_site = self.headers.get("Sec-Fetch-Site", "")
        if (
            origin and not origin_matches_request(origin, self.headers.get("Host", ""))
        ) or (fetch_site and fetch_site not in {"same-origin", "none"}):
            self._send_api_error(403, "origin_not_allowed", send_body)
            return
        original = re.fullmatch(
            r"/api/v1/documents/(api-document-[a-f0-9]{24})/original", path
        )
        if original:
            if query:
                self._send_api_error(400, "unknown_parameter", send_body)
                return
            opened = open_verified_original(API_DB, original.group(1))
            if not opened:
                self._send_api_error(404, "original_not_available", send_body)
                return
            descriptor, size, mime, filename = opened
            try:
                self.send_response(200)
                self.send_header("Content-Type", mime)
                self.send_header("Content-Length", str(size))
                self.send_header("Content-Disposition", f"inline; filename={filename}")
                self.send_header("Cache-Control", "no-store")
                self.send_header("Pragma", "no-cache")
                self.send_header("X-Content-Type-Options", "nosniff")
                self.send_header("X-Frame-Options", "DENY")
                self.send_header("Referrer-Policy", "no-referrer")
                self.send_header(
                    "Content-Security-Policy",
                    "default-src 'none'; frame-ancestors 'none'",
                )
                self.end_headers()
                if send_body:
                    with os.fdopen(descriptor, "rb", closefd=False) as handle:
                        while data := handle.read(64 * 1024):


Output:
Chunk ID: f554dc
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 3443
Output:
            positive_hits TEXT,
            negative_hits TEXT,
            source TEXT,
            computed_at TEXT DEFAULT CURRENT_TIMESTAMP
        );

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
);
    if max_score >= 3 or max_score == 2 or load >= 4:
        return "orange"
    if max_score == 1 or load >= 1:
        return "yellow"
    if unknown:
        return "unknown"
    return "green"

def sync_date(day: str, token: str, force: bool = False) -> tuple[bool, str]:
    items = fetch_items(day, token)
    payload = json.dumps(items, ensure_ascii=False, sort_keys=True, default=str)
    fp = hashlib.sha256(payload.encode()).hexdigest()
    con = sqlite3.connect(DB_PATH)
    con.row_factory = sqlite3.Row
    try:
        ensure_state_table(con)
        old = con.execute("SELECT value FROM sync_state WHERE key=?", (f"yazio:{day}:v2:fingerprint",)).fetchone()
        if old and old["value"] == fp and not force:
            return False, f"{day}: unverändert"

        # Replace this day's automated YAZIO rows only.
        ids = [r[0] for r in con.execute("SELECT id FROM nutrition_items WHERE datum=? AND source='yazio_api'", (day,))]
        if ids:
            q = ",".join("?" for _ in ids)
            con.execute(f"DELETE FROM nutrition_item_nutrients WHERE item_id IN ({q})", ids)
            con.execute(f"DELETE FROM nutrition_histamine_scores WHERE item_id IN ({q})", ids)
        con.execute("DELETE FROM nutrition_items WHERE datum=? AND source='yazio_api'", (day,))
        con.execute("DELETE FROM nutrition_meal_summary WHERE datum=?", (day,))
        con.execute("DELETE FROM nutrition_daily_summary_v2 WHERE datum=?", (day,))
        con.execute("DELETE FROM ernaehrung WHERE datum=? AND COALESCE(notizen,'') LIKE '%Quelle: YAZIO API%'", (day,))
        con.execute("DELETE FROM tagebuch WHERE datum=? AND COALESCE(verknuepft,'')='yazio_api'", (day,))

        if not items:
            con.execute("""
                INSERT INTO sync_state(key,value,updated_at) VALUES(?,?,CURRENT_TIMESTAMP)
                ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=CURRENT_TIMESTAMP
            """, (f"yazio:{day}:v2:fingerprint", fp))
            con.commit()
            return False, f"{day}: keine Einträge"

        inserted_ids: list[int] = []
        for it in items:
            nutrients = it["nutrients"] or {}
            kcal = fnum(nutrients.get("energy.energy"))
            protein = fnum(nutrients.get("nutrient.protein"))
            carb = fnum(nutrients.get("nutrient.carb"))
            fat = fnum(nutrients.get("nutrient.fat"))
            ihash = item_hash(day, it)
            cur = con.execute("""
                INSERT INTO nutrition_items(source, source_item_id, source_product_id, datum, meal, name, brand, amount, amount_unit, serving, serving_quantity,
                    kcal, protein_g, carb_g, fat_g, raw_item_json, raw_product_json, item_hash, updated_at)
                VALUES('yazio_api',?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,CURRENT_TIMESTAMP)
            """, (it["source_item_id"], it["source_product_id"], day, it["meal"], it["name"], it.get("brand"), it["amount"], it.get("amount_unit") or "g", it.get("serving"), it.get("serving_quantity"), kcal, protein, carb, fat, json.dumps(it["raw_item_json"], ensure_ascii=False, sort_keys=True, default=str), json.dumps(it["raw_product_json"], ensure_ascii=False, sort_keys=True, default=str), ihash))
            item_id = int(cur.lastrowid)
            inserted_ids.append(item_id)
            for key, val in sorted(nutrients.items()):
                con.execute("INSERT OR REPLACE INTO nutrition_item_nutrients(item_id,nutrient_key,value,unit,raw_value) VALUES(?,?,?,?,?)", (item_id, key, fnum(val), NUTRIENT_UNITS.get(key), str(val)))
            hs = classify_histamine(con, it["name"])
            con.execute("""
                INSERT OR REPLACE INTO nutrition_histamine_scores(item_id,canonical_food,sighi_score,traffic_light,tags,confidence,reason,scored_at)
                VALUES(?,?,?,?,?,?,?,CURRENT_TIMESTAMP)
            """, (item_id, hs["canonical_food"], hs["sighi_score"], hs["traffic_light"], hs["tags"], hs["confidence"], hs["reason"]))
            desc = f"{it['name']} ({it['amount']:g} g): {kcal:g} kcal, {protein:g} g Protein, {carb:g} g KH, {fat:g} g Fett"
            con.execute("INSERT INTO ernaehrung(datum,mahlzeit,beschreibung,wirkung,notizen) VALUES(?,?,?,?,?)", (day, it["meal"], desc, "", f"Quelle: YAZIO API; Typ: {it['source_type']}; ID: {it['source_product_id']}"))

        recompute_summaries(con, day)
        make_legacy_diary(con, day)
        con.execute("""
            INSERT INTO sync_state(key,value,updated_at) VALUES(?,?,CURRENT_TIMESTAMP)
            ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=CURRENT_TIMESTAMP
        """, (f"yazio:{day}:v2:fingerprint", fp))
        con.commit()
        return True, f"{day}: {len(inserted_ids)} Einträge importiert/aktualisiert"
    finally:
        con.close()

def recompute_summaries(con: sqlite3.Connection, day: str) -> None:
    item_rows = list(con.execute("""
        SELECT i.id, i.meal, i.kcal, i.protein_g, i.carb_g, i.fat_g,
               h.sighi_score, h.traffic_light
        FROM nutrition_items i
        LEFT JOIN nutrition_histamine_scores h ON h.item_id=i.id
        WHERE i.datum=? AND i.source='yazio_api'
    """, (day,)))
    nutrient_rows = list(con.execute("""
        SELECT i.meal, n.nutrient_key, SUM(n.value) value
        FROM nutrition_items i JOIN nutrition_item_nutrients n ON n.item_id=i.id
        WHERE i.datum=? AND i.source='yazio_api'
        GROUP BY i.meal, n.nutrient_key
    """, (day,)))
    by_meal: dict[str, dict[str, Any]] = defaultdict(lambda: {"kcal":0.0,"protein":0.0,"carb":0.0,"fat":0.0,"items":0,"nutrients":defaultdict(float),"scores":[],"unknown":0,"load":0.0})
    for r in item_rows:
        m = r["meal"] or "unknown"
        b = by_meal[m]
        b["items"] += 1
        b["kcal"] += fnum(r["kcal"]); b["protein"] += fnum(r["protein_g"]); b["carb"] += fnum(r["carb_g"]); b["fat"] += fnum(r["fat_g"])
        if r["sighi_score"] is None:
            b["unknown"] += 1
        else:
            sc = int(r["sighi_score"]); b["scores"].append(sc); b["load"] += sc
    for r in nutrient_rows:
        by_meal[r["meal"] or "unknown"]["nutrients"][r["nutrient_key"]] += fnum(r["value"])

    day_total = {"kcal":0.0,"protein":0.0,"carb":0.0,"fat":0.0,"items":0,"nutrients":defaultdict(float),"scores":[],"unknown":0,"load":0.0}
    for m, b in by_meal.items():
        max_score = max(b["scores"] or [0])
        label = score_label(max_score, b["unknown"], b["load"])
        con.execute("""
            INSERT OR REPLACE INTO nutrition_meal_summary(datum,meal,kcal,protein_g,carb_g,fat_g,item_count,nutrient_json,histamine_score,histamine_label,updated_at)
            VALUES(?,?,?,?,?,?,?,?,?,?,CURRENT_TIMESTAMP)
        """, (day, m, round(b["kcal"],1), round(b["protein"],1), round(b["carb"],1), round(b["fat"],1), int(b["items"]), json.dumps(dict(sorted(b["nutrients"].items())), ensure_ascii=False), round(b["load"],1), label))
        day_total["kcal"] += b["kcal"]; day_total["protein"] += b["protein"]; day_total["carb"] += b["carb"]; day_total["fat"] += b["fat"]
        day_total["items"] += int(b["items"]); day_total["unknown"] += b["unknown"]; day_total["load"] += b["load"]; day_total["scores"].extend(b["scores"])
        for k, v in b["nutrients"].items(): day_total["nutrients"][k] += v
    max_score = max(day_total["scores"] or [0])
    label = score_label(max_score, day_total["unknown"], day_total["load"])
    con.execute("""
        INSERT OR REPLACE INTO nutrition_daily_summary_v2(datum,kcal,protein_g,carb_g,fat_g,item_count,nutrient_json,histamine_score,histamine_max,histamine_unknown_count,histamine_label,updated_at)
        VALUES(?,?,?,?,?,?,?,?,?,?,?,CURRENT_TIMESTAMP)
    """, (day, round(day_total["kcal"],1), round(day_total["protein"],1), round(day_total["carb"],1), round(day_total["fat"],1), int(day_total["items"]), json.dumps(dict(sorted(day_total["nutrients"].items())), ensure_ascii=False), round(day_total["load"],1), max_score, day_total["unknown"], label))

def make_legacy_diary(con: sqlite3.Connection, day: str) -> None:
    rows = list(con.execute("""
        SELECT i.meal,i.name,i.amount,i.kcal,i.protein_g,i.carb_g,i.fat_g,h.traffic_light,h.canonical_food,h.sighi_score
        FROM nutrition_items i LEFT JOIN nutrition_histamine_scores h ON h.item_id=i.id
        WHERE i.datum=? AND i.source='yazio_api'
        ORDER BY CASE i.meal WHEN 'breakfast' THEN 1 WHEN 'lunch' THEN 2 WHEN 'dinner' THEN 3 WHEN 'snack' THEN 4 ELSE 9 END, i.id
    """, (day,)))
    totals = con.execute("SELECT * FROM nutrition_daily_summary_v2 WHERE datum=?", (day,)).fetchone()
    lines = []
    for r in rows:
        light = {"green":"🟢","yellow":"🟡","orange":"🟠","red":"🔴","unknown":"⚫"}.get(r["traffic_light"] or "unknown", "⚫")
        lines.append(f"- [{r['meal']}] {light} {r['name']} ({fnum(r['amount']):g} g): {fnum(r['kcal']):g} kcal, {fnum(r['protein_g']):g} g Protein, {fnum(r['carb_g']):g} g KH, {fnum(r['fat_g']):g} g Fett")
    if totals:
        summary = f"YAZIO-Ernährung {day}\n\n" + "\n".join(lines) + f"\n\nSumme: {totals['kcal']:.1f} kcal | {totals['protein_g']:.1f} g Protein | {totals['carb_g']:.1f} g Kohlenhydrate | {totals['fat_g']:.1f} g Fett | Histamin: {totals['histamine_label']} ({totals['histamine_score']:.1f})"
    else:
        summary = f"YAZIO-Ernährung {day}\n\n" + "\n".join(lines)
    fp = hashlib.sha256(summary.encode()).hexdigest()
    con.execute("""
        INSERT INTO tagebuch(datum,kategori,titel,inhalt,wirkung,verknuepft,notizen)
        VALUES(?,?,?,?,?,?,?)
    """, (day, "ernaehrung", f"YAZIO-Ernährung: {day}", summary, "", "yazio_api", f"Automatisch via YAZIO API v2; fingerprint={fp}"))

def daterange(start: date, end: date) -> list[date]:
    if end < start:
        start, end = end, start
    days = []
    d = start


Output:
Chunk ID: 6288cc
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 4432
Output:
"""Deterministic, synthetic-only SQLite fixture for Dashboard v5 acceptance work.

This module deliberately knows nothing about a Dashboard v5 Python/UI API.  It only
materializes the canonical schema and representative rows at a caller-provided
path.  Dates are derived from an explicit anchor so freshness tests do not depend
on wall-clock time.
"""
from __future__ import annotations

import argparse
import json
import sqlite3
import tempfile
from datetime import date, datetime, time, timedelta, timezone
from pathlib import Path
from typing import Final

ROOT: Final = Path(__file__).resolve().parents[2]
DEFAULT_SCHEMA: Final = ROOT / "database" / "schema.sql"
SYMPTOM_DIMENSIONS: Final = (
    "Aphthen/Mundulzera",
    "GI/Darm",
    "Müdigkeit/Fatigue",
    "Haut",
    "Augen",
    "Gelenke",
    "Vaskulär/Thrombose-Warnzeichen",
)
SPRINT4B_LAGS: Final = (0, 1, 2, 3)
SPRINT4B_PHASES: Final = (
    "baseline_pre_treatment", "early_treatment", "stable_treatment", "unknown",
)
SPRINT4B_PREDICTORS: Final = ("apple:heart_rate_variability", "nutrition:histamine_score")
SPRINT4B_METHOD: Final = "spearman_7d_block_permutation_complete_case_phase_stratified_bh_v1"
SPRINT4B_BASE_FLAGS: Final = [
    "complete_case", "phase_stratified", "7d_block_permutation", "no_causality",
]
SYNTHETIC_FIXTURE_MARKER: Final = "dashboard-v5-synthetic-fixture-v1"

def sprint4b_correlation_fixture() -> dict[str, object]:
    """Return a deterministic engine-compatible, aggregate-only Sprint-4B grid."""
    results: list[dict[str, object]] = []
    for predictor in SPRINT4B_PREDICTORS:
        for lag in SPRINT4B_LAGS:
            for phase in SPRINT4B_PHASES:
                status = "computed"
                expected, eligible, observed = 24, 24 - lag, 18 - lag
                rho: float | None = round(0.42 - lag * 0.08, 2)
                p_value: float | None = round(0.01 + lag * 0.02, 2)
                q_value: float | None = round(0.03 + lag * 0.03, 2)
                flags = list(SPRINT4B_BASE_FLAGS)
                if predictor == SPRINT4B_PREDICTORS[0] and lag == 0 and phase == SPRINT4B_PHASES[0]:
                    rho, p_value, q_value = 0.0, 0.0, 0.0
                elif predictor == SPRINT4B_PREDICTORS[0] and lag == 3 and phase == "early_treatment":
                    status, observed = "insufficient_n", 3
                    rho = p_value = q_value = None
                    flags.append("low_n_or_variance")
                elif predictor == SPRINT4B_PREDICTORS[1] and lag == 2 and phase == "stable_treatment":
                    status, expected, eligible, observed = "insufficient_coverage", 24, 10, 0
                    rho = p_value = q_value = None
                    flags.append("low_target_coverage")
                results.append({
                    "id": {"predictor": predictor, "target": "symptom_total"},
                    "dimensions": {"lag_days": lag, "medication_phase": phase},
                    "sample": {
                        "eligible_target_days": eligible,
                        "expected_target_days": expected,
                        "observed_pairs": observed,
                        "missing_pairs": eligible - observed,
                        "target_coverage": round(eligible / expected, 6),
                    },
                    "statistics": {"rho": rho, "p_value": p_value, "q_value": q_value},
                    "status": status,
                    "method": SPRINT4B_METHOD,
                    "quality_flags": flags,
                })
    return {
        "dimensions": {
            "lag_days": list(SPRINT4B_LAGS),
            "medication_phases": list(SPRINT4B_PHASES),
            "lag_direction": "predictor_d_to_target_d_plus_lag",
        },
        "results": results,
    }

def _iso_day(day: date) -> str:
    return day.isoformat()

def _iso_noon(day: date) -> str:
    return datetime.combine(day, time(12), tzinfo=timezone.utc).isoformat()

def build_dashboard_v5_fixture(
    db_path: str | Path,
    *,
    anchor_date: date = date(2026, 6, 15),
    schema_path: str | Path = DEFAULT_SCHEMA,
) -> dict[str, object]:
    """Create a new synthetic database and return its deterministic case manifest.

    ``db_path`` must not already exist.  This fail-closed behavior prevents this
    helper from ever replacing a real database accidentally.
    """
    target = Path(db_path)
    schema = Path(schema_path)
    if target.exists():
        raise FileExistsError(f"refusing to replace existing database: {target}")
    if not schema.is_file():
        raise FileNotFoundError(f"schema not found: {schema}")
    target.parent.mkdir(parents=True, exist_ok=True)

    current = anchor_date - timedelta(days=1)
    stale = anchor_date - timedelta(days=45)
    future = anchor_date + timedelta(days=2)
    complete_symptom_days = [anchor_date - timedelta(days=offset) for offset in range(1, 9)]
    incomplete_symptom_day = anchor_date - timedelta(days=9)
    no_data_day = anchor_date - timedelta(days=10)

    connection = sqlite3.connect(target)
    try:
        connection.executescript(schema.read_text(encoding="utf-8"))
        connection.execute(
            "CREATE TABLE dashboard_v5_synthetic_fixture (marker TEXT PRIMARY KEY)"
        )
        connection.execute(
            "INSERT INTO dashboard_v5_synthetic_fixture(marker) VALUES (?)",
            (SYNTHETIC_FIXTURE_MARKER,),
        )
        connection.executemany(
            """INSERT INTO apple_health_records
               (record_type,metric,start_date,end_date,value,value_text,unit,
                source_name,source_version,device,file_name,file_hash,record_hash,
                raw_json,imported_at)
               VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
            [
                ("HKQuantityType", "heart_rate", _iso_noon(current), _iso_noon(current), 64.0, None, "count/min", "Synthetic Current Source", "1", "Synthetic Device", "synthetic-current.json", "fixture-current-file", "fixture-current-record", "{}", _iso_noon(current)),
                ("HKQuantityType", "step_count", _iso_noon(stale), _iso_noon(stale), 1234.0, None, "count", "Synthetic Stale Source", "1", "Synthetic Device", "synthetic-stale.json", "fixture-stale-file", "fixture-stale-record", "{}", _iso_noon(stale)),
                ("HKQuantityType", "resting_heart_rate", _iso_noon(future), _iso_noon(future), 61.0, None, "count/min", "Synthetic Future Source", "1", "Synthetic Device", "synthetic-future.json", "fixture-future-file", "fixture-future-record", "{}", _iso_noon(future)),
                ("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)),
            ],
        )
        explorer_rows = []
        for offset in range(1, 23):
            day = anchor_date - timedelta(days=offset)
            for metric, value, unit in (
                ("heart_rate_variability", 42.0 + offset, "ms"),
                ("resting_heart_rate", 60.0 + offset, "count/min"),
                ("sleep_analysis", 6.5 + offset / 10, "hr"),
                ("step_count", 5000.0 + offset * 100, "count"),
            ):
                if metric == "sleep_analysis" and offset == 4:
                    continue
                key = f"fixture-explorer-{metric}-{offset}"
                explorer_rows.append((
                    "HKQuantityType", metric, _iso_noon(day), _iso_noon(day), value, None, unit,
                    "Synthetic Explorer Source", "1", "Synthetic Device", f"{key}.json", key,
                    f"{key}-record", "{}", _iso_noon(day),
                ))
        connection.executemany(
            """INSERT INTO apple_health_records
               (record_type,metric,start_date,end_date,value,value_text,unit,
                source_name,source_version,device,file_name,file_hash,record_hash,
                raw_json,imported_at)
               VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
            explorer_rows,
        )
        correlation_results = sprint4b_correlation_fixture()["results"]
        fixture_timestamp = _iso_noon(anchor_date)
        assert isinstance(correlation_results, list)
        for result in correlation_results:
            assert isinstance(result, dict)
            connection.execute(
                """INSERT INTO multimodal_correlation_results
                   (predictor,target,lag_days,medication_phase,n,eligible_target_days,
                    expected_target_days,target_coverage,missing_pairs,rho,p_value,q_value,status,
                    method,quality_flags,interpretation,computed_at)
                   VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
                (
                    result["id"]["predictor"], result["id"]["target"],
                    result["dimensions"]["lag_days"], result["dimensions"]["medication_phase"],
                    result["sample"]["observed_pairs"], result["sample"]["eligible_target_days"],
                    result["sample"]["expected_target_days"], result["sample"]["target_coverage"],
                    result["sample"]["missing_pairs"], result["statistics"]["rho"],
                    result["statistics"]["p_value"], result["statistics"]["q_value"],
                    result["status"], result["method"], json.dumps(result["quality_flags"]),
                    "Kein belastbares korrigiertes Signal; explorativ und keine Kausalität."
                    if result["status"] == "computed"
                    else (
                        "Unzureichende Zielabdeckung; keine Korrelation berechnet und keine Kausalität."
                        if result["status"] == "insufficient_coverage"
                        else "Unzureichende vollständige Datenpaare oder Varianz; keine Kausalität."
                    ),
                    fixture_timestamp,
                ),
            )
        connection.execute(
            """INSERT INTO laborwerte
               (parameter_name,wert,einheit,reference_min,reference_max,
                abnahme_datum,befund_datum,wert_original,quelle,
                validierungsstatus,source_type,reference_range_source,
                verified_against_original,provenance_note,ermittlung_datum)
               VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
            (
                "C-Reaktives Protein (CRP)",
                "12.5", "mg/L", "0", "5", _iso_day(stale), _iso_day(stale), "12.5",
                "synthetic laboratory fixture", "validiert", "synthetic",
                "scanned_original", 1, "Synthetic test row; no person or source document.", fixture_timestamp,
            ),
        )
        connection.executemany(
            """INSERT INTO medication_administrations
               (datum,medication_name,dose,route,event_type,scheduled_next_date,notes,source,created_at)
               VALUES(?,?,?,?,?,?,?,?,?)""",
            [
                (_iso_day(anchor_date - timedelta(days=4)), "SYNTHETIC_EVENT_ON_MEASUREMENT_GAP", "10 mg", "synthetic", "administered", None, "timeline gap fixture", "fixture", fixture_timestamp),
                (_iso_day(current), "SYNTHETIC_ADMINISTERED_MEDICATION", "10 mg", "synthetic", "administered", None, "display as administered only", "fixture", fixture_timestamp),
                (_iso_day(anchor_date + timedelta(days=1)), "SYNTHETIC_PLANNED_MEDICATION_WITH_A_VERY_LONG_LABEL_FOR_RESPONSIVE_LAYOUT", "10 mg", "synthetic", "planned", _iso_day(anchor_date + timedelta(days=1)), "must not be represented as taken", "fixture", fixture_timestamp),
                (_iso_day(anchor_date + timedelta(days=1)), "SYNTHETIC_CANCELLED_MEDICATION", "10 mg", "synthetic", "cancelled", None, "must not be represented as taken", "fixture", fixture_timestamp),
            ],
        )
        connection.executemany(
            """INSERT INTO nutrition_items
               (id,source,source_item_id,datum,meal,name,brand,amount,amount_unit,
                kcal,protein_g,carb_g,fat_g,item_hash,imported_at,updated_at)
               VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
            [
                (1, "fixture", "complete", _iso_day(current), "lunch", "SYNTHETIC_COMPLETE_FOOD_MAPPING", "Synthetic Brand", 100, "g", 0, 0, 0, 0, "fixture-food-complete", fixture_timestamp, fixture_timestamp),
                (2, "fixture", "incomplete", _iso_day(current), "dinner", "SYNTHETIC_INCOMPLETE_FOOD_MAPPING_WITH_A_VERY_LONG_NAME_THAT_MUST_WRAP", None, None, "g", None, None, None, None, "fixture-food-incomplete", fixture_timestamp, fixture_timestamp),
            ],
        )
        connection.executemany(
            """INSERT INTO nutrition_histamine_scores
               (item_id,canonical_food,sighi_score,traffic_light,tags,confidence,reason,scored_at)
               VALUES(?,?,?,?,?,?,?,?)""",
            [
                (1, "synthetic cucumber", 0, "green", "fixture", "high", "complete synthetic mapping", fixture_timestamp),
                (2, None, None, "unknown", None, "low", "intentionally unmapped fixture row", fixture_timestamp),
            ],
        )
        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,updated_at)
               VALUES(?,?,?,?,?,?,?,?,?,?,?)""",
            (_iso_day(current), None, None, None, None, 2, None, 0, 1, "unknown", _iso_noon(current)),
        )
        symptom_rows = []
        for day_index, day in enumerate(complete_symptom_days):
            for dimension_index, dimension in enumerate(SYMPTOM_DIMENSIONS):
                symptom_rows.append((_iso_day(day), dimension, str((day_index + dimension_index) % 4), "daily_quick_score", "synthetic complete day", fixture_timestamp))
        for dimension in SYMPTOM_DIMENSIONS[:-1]:
            symptom_rows.append((_iso_day(incomplete_symptom_day), dimension, "1", "daily_quick_score", "synthetic incomplete day", fixture_timestamp))
        connection.executemany(
            "INSERT INTO symptom_log(datum,symptom,schwergrad,kontext,notizen,created_at) VALUES(?,?,?,?,?,?)",
            symptom_rows,
        )
        connection.commit()
        fk_errors = connection.execute("PRAGMA foreign_key_check").fetchall()
        if fk_errors:
            raise RuntimeError(f"fixture violates foreign keys: {fk_errors}")
    except Exception:
        connection.close()
        target.unlink(missing_ok=True)
        raise
    finally:
        try:
            connection.close()
        except Exception:
            pass
    target.chmod(0o600)

    return {
        "anchor_date": _iso_day(anchor_date),
        "current_source_date": _iso_day(current),
        "stale_source_date": _iso_day(stale),
        "future_source_date": _iso_day(future),
        "complete_symptom_days": [_iso_day(day) for day in complete_symptom_days],
        "incomplete_symptom_day": _iso_day(incomplete_symptom_day),
        "no_data_day": _iso_day(no_data_day),
        "synthetic_only": True,
    }

def validate_fixture(db_path: str | Path) -> dict[str, object]:
    """Return structural acceptance checks without importing application code."""
    db = Path(db_path)
    connection = sqlite3.connect(f"file:{db}?mode=ro", uri=True)
    try:
        counts = {
            table: connection.execute(f"SELECT COUNT(*) FROM {table}").fetchone()[0]
            for table in (
                "apple_health_records", "laborwerte", "medication_administrations",
                "nutrition_items", "nutrition_histamine_scores", "symptom_log",
            )
        }
        checks = {
            "integrity": connection.execute("PRAGMA integrity_check").fetchone()[0],
            "foreign_key_errors": connection.execute("PRAGMA foreign_key_check").fetchall(),
            "verified_lab_outliers": connection.execute(
                """SELECT COUNT(*) FROM laborwerte
                   WHERE verified_against_original=1 AND CAST(wert AS REAL)>CAST(reference_max AS REAL)"""
            ).fetchone()[0],
            "complete_symptom_days": connection.execute(
                """SELECT COUNT(*) FROM (SELECT datum FROM symptom_log
                   WHERE kontext='daily_quick_score' GROUP BY datum HAVING COUNT(DISTINCT symptom)=7)"""
            ).fetchone()[0],
            "medication_states": sorted(row[0] for row in connection.execute(
                "SELECT DISTINCT event_type FROM medication_administrations"
            )),
            "food_mapping_states": connection.execute(
                """SELECT SUM(canonical_food IS NOT NULL AND sighi_score IS NOT NULL),
                          SUM(canonical_food IS NULL OR sighi_score IS NULL)
                   FROM nutrition_histamine_scores"""
            ).fetchone(),
        }
    finally:
        connection.close()
    return {"counts": counts, "checks": checks}

def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("output", nargs="?", help="new SQLite path; omit for a temporary fixture")
    parser.add_argument("--anchor-date", type=date.fromisoformat, default=date(2026, 6, 15))
    args = parser.parse_args()
    if args.output:
        output = Path(args.output)
    else:
        output = Path(tempfile.mkdtemp(prefix="dashboard-v5-fixture-")) / "health.db"
    manifest = build_dashboard_v5_fixture(output, anchor_date=args.anchor_date)
    print(json.dumps({"path": str(output), "manifest": manifest, "validation": validate_fixture(output)}, indent=2, default=list))

if __name__ == "__main__":
    main()


I’m making the edits now: renderer assets/markup first, then the shared ECharts helper and V5 controller migration, then API/worker/schema/docs/tests.


rg -n "def dispatch_api|API_PATHS|parse_query\(" scripts/health/dashboard_v5/read_api.py && sed -n '1455,1740p' scripts/health/dashboard_v5/read_api.py


Output:
Chunk ID: 4be67e
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 3264
Output:
77:API_PATHS = frozenset(
146:def parse_query(query: str, allowed: set[str]) -> dict[str, str]:
2245:def dispatch_api(database: Path, path: str, query: str) -> dict[str, Any]:
2258:        path not in API_PATHS
2267:            parse_query(query, set())
2270:            return _record_labs(connection, parse_query(query, {"q", "from", "to"}))
2272:            return _record_medications(connection, parse_query(query, {"from", "to"}))
2275:                connection, parse_query(query, {"from", "to", "order", "institution"})
2280:                parse_query(
2300:                    connection, opaque, parse_query(query, {"q"})
2305:                parse_query(query, {"cursor", "limit"}),
2309:                connection, parse_query(query, {"from", "to", "sections"})
2312:            params = parse_query(query, {"q"})
2338:            params = parse_query(query, {"metric", "from", "to", "resolution"})
2350:            params = parse_query(query, {"from", "to"})
2353:            params = parse_query(query, {"from", "to", "types"})
2356:            parse_query(query, set())
2366:            params = parse_query(query, {"q"})
_RECORD_SORT_SPECS = {
    "document_date_desc": ("document_date", "desc", "desc"),
    "document_date_asc": ("document_date", "asc", "asc"),
    "import_date_desc": ("upload_datum", "desc", "desc"),
    "category": ("kategorie", "asc", "desc"),
    "institution": ("institution", "asc", "desc"),
    "type": ("daten_typ", "asc", "desc"),
    "review_status": ("review_status", "asc", "desc"),
}

def _cursor_binding(params: dict[str, str], *, search: bool) -> str:
    bound: dict[str, Any] = {
        key: params.get(key, "")
        for key in (
            "from",
            "to",
            "category",
            "institution",
            "type",
            "review_status",
            "q",
            "sort",
        )
    }
    bound["search"] = search
    return hashlib.sha256(
        json.dumps(bound, sort_keys=True, separators=(",", ":")).encode("utf-8")
    ).hexdigest()

def _encode_record_cursor(payload: dict[str, Any]) -> str:
    raw = json.dumps(
        {"v": _RECORD_CURSOR_VERSION, **payload},
        sort_keys=True,
        separators=(",", ":"),
    ).encode("utf-8")
    signature = hmac.new(_RECORD_CURSOR_KEY, raw, hashlib.sha256).digest()[:16]
    return "record-cursor-" + base64.urlsafe_b64encode(raw + signature).decode(
        "ascii"
    ).rstrip("=")

def _decode_record_cursor(value: str, binding: str) -> dict[str, Any]:
    if not value.startswith("record-cursor-") or len(value) > 600:
        raise APIError(400, "invalid_cursor")
    encoded = value.removeprefix("record-cursor-")
    try:
        packed = base64.urlsafe_b64decode(encoded + "=" * (-len(encoded) % 4))
        raw, supplied = packed[:-16], packed[-16:]
        expected = hmac.new(_RECORD_CURSOR_KEY, raw, hashlib.sha256).digest()[:16]
        payload = json.loads(raw)
    except (ValueError, UnicodeError, json.JSONDecodeError) as error:
        raise APIError(400, "invalid_cursor") from error
    if not hmac.compare_digest(supplied, expected):
        raise APIError(400, "invalid_cursor")
    if payload.get("v") != _RECORD_CURSOR_VERSION or payload.get("b") != binding:
        raise APIError(400, "cursor_mismatch")
    if not isinstance(payload.get("id"), int) or payload["id"] < 1:
        raise APIError(400, "invalid_cursor")
    return payload

def _record_page_size(params: dict[str, str]) -> int:
    raw = params.get("limit", str(RECORD_PAGE_SIZE))
    if not raw.isdigit():
        raise APIError(400, "invalid_limit")
    size = int(raw)
    if not 1 <= size <= RECORD_MAX_PAGE_SIZE:
        raise APIError(400, "invalid_limit")
    return size

def _record_document_rows(
    connection: sqlite3.Connection, params: dict[str, str]
) -> dict[str, Any]:
    """Apply document filters and FTS/review join in SQLite before page limiting."""
    if not table_exists(connection, "dokumente"):
        return {"documents": [], "next_cursor": None, "truncated": False, "total": 0}
    start, end = parse_range(params)
    filters = ["1=1"]
    values: list[Any] = []
    if start:
        filters.append("d.document_date>=?")
        values.append(start.isoformat())
    if end:
        filters.append("d.document_date<=?")
        values.append(end.isoformat())
    for key, column, maximum in (
        ("category", "kategorie", 80),
        ("institution", "institution", 120),
        ("type", "daten_typ", 20),
        ("review_status", "review_status", 32),
    ):
        raw = params.get(key)
        if raw:
            clean = safe_metadata_text(raw, maximum, allow_empty=False)
            if clean is None:
                raise APIError(400, "filter_not_allowed")
            filters.append(f"d.{column}=?")
            values.append(clean)
    sort = params.get("sort", "document_date_desc")
    if sort not in RECORD_DOCUMENT_SORTS:
        raise APIError(400, "sort_not_allowed")
    page_size = _record_page_size(params)
    query_text = params.get("q")
    search = bool(query_text)
    binding = _cursor_binding(params, search=search)
    cursor_payload = (
        _decode_record_cursor(params["cursor"], binding)
        if params.get("cursor")
        else None
    )
    join = ""
    select_snippet = "NULL AS snippet"
    select_rank = "NULL AS search_rank"
    order = RECORD_DOCUMENT_SORTS[sort].replace("id", "d.id")
    query_values = list(values)
    search_cursor_clause = ""
    search_cursor_values: list[Any] = []
    if search:
        if not table_exists(connection, "health_document_fts"):
            raise APIError(503, "fts_unavailable")
        tokens = _fts_tokens(query_text or "")
        join = " JOIN health_document_fts f ON CAST(d.id AS TEXT)=f.document_id"
        filters.append("d.review_status='geprueft'")
        filters.append("health_document_fts MATCH ?")
        query_values.append(_fts_literal_query(tokens))
        select_snippet = "snippet(health_document_fts, 6, '', '', '…', ?) AS snippet"
        select_rank = "bm25(health_document_fts) AS search_rank"
        order = "search_rank ASC, d.id DESC"
        if cursor_payload:
            rank = cursor_payload.get("k")
            if not isinstance(rank, (int, float)):
                raise APIError(400, "invalid_cursor")
            search_cursor_clause = "WHERE (search_rank>? OR (search_rank=? AND id<?))"
            search_cursor_values.extend(
                (float(rank), float(rank), cursor_payload["id"])
            )
    else:
        column, primary_direction, id_direction = _RECORD_SORT_SPECS[sort]
        if cursor_payload:
            primary = cursor_payload.get("k")
            if not isinstance(primary, str):
                raise APIError(400, "invalid_cursor")
            primary_operator = ">" if primary_direction == "asc" else "<"
            id_operator = ">" if id_direction == "asc" else "<"
            collation = " COLLATE NOCASE" if sort in {"category", "institution"} else ""
            filters.append(
                f"(COALESCE(d.{column},''){collation}{primary_operator}? OR "
                f"(COALESCE(d.{column},''){collation}=?{collation} AND d.id{id_operator}?))"
            )
            query_values.extend((primary, primary, cursor_payload["id"]))
    where = " AND ".join(filters)
    if search:
        sql = f"""WITH raw_hits AS MATERIALIZED (
                        SELECT d.id,d.document_date,d.upload_datum,d.kategorie,
                               d.institution,d.daten_typ,d.review_status,
                               d.processing_quality,d.groessekbytes,d.extrahierte_inhalte,
                               d.local_original_path,d.dateipfad,f.chunk_no,
                               {select_snippet},{select_rank}
                        FROM dokumente d{join}
                        WHERE {where}
                    ), best_rank AS (
                        SELECT id,MIN(search_rank) AS search_rank
                        FROM raw_hits GROUP BY id
                    ), selected AS (
                        SELECT r.* FROM raw_hits r
                        JOIN best_rank b ON b.id=r.id AND b.search_rank=r.search_rank
                        WHERE r.chunk_no=(
                            SELECT MIN(r2.chunk_no) FROM raw_hits r2
                            WHERE r2.id=r.id AND r2.search_rank=r.search_rank
                        )
                    )
                    SELECT * FROM selected {search_cursor_clause}
                    ORDER BY search_rank ASC,id DESC LIMIT ?"""
        parameters = [
            RECORD_MAX_SNIPPET_CHARS,
            *query_values,
            *search_cursor_values,
            page_size + 1,
        ]
    else:
        sql = f"""SELECT d.id,d.document_date,d.upload_datum,d.kategorie,d.institution,
                         d.daten_typ,d.review_status,d.processing_quality,d.groessekbytes,
                         d.extrahierte_inhalte,d.local_original_path,d.dateipfad,
                         {select_snippet},{select_rank}
                  FROM dokumente d{join}
                  WHERE {where}
                  ORDER BY {order} LIMIT ?"""
        parameters = [*query_values, page_size + 1]
    try:
        rows = connection.execute(sql, parameters).fetchall()
    except sqlite3.OperationalError as error:
        if search:
            raise APIError(400, "query_not_allowed") from error
        raise
    has_more = len(rows) > page_size
    result: list[dict[str, Any]] = []
    for row in rows[:page_size]:
        category = safe_metadata_text(row["kategorie"], 80, allow_empty=True)
        institution = safe_metadata_text(row["institution"], 120, allow_empty=True)
        if category is None or institution is None:
            continue
        document_day = parse_day(row["document_date"])
        reviewed = row["review_status"] == "geprueft"
        chunks = document_chunks(row["extrahierte_inhalte"]) if reviewed else []
        content_status = (
            "not_reviewed"
            if not reviewed
            else "available"
            if chunks
            else "no_extracted_content"
        )
        original = probe_original(
            row["local_original_path"] or row["dateipfad"], reviewed=reviewed
        )
        original_status = original.status
        original.close()
        result.append(
            {
                "id": api_document_id(row["id"], document_day, category, institution),
                "title": f"Dokument · {category or 'Ohne Kategorie'}",
                "document_date": document_day,
                "document_date_status": "known" if document_day else "unknown",
                "import_date": parse_day(row["upload_datum"]),
                "category": category,
                "institution": institution,
                "type": row["daten_typ"]
                if row["daten_typ"] in {"pdf", "image"}
                else "unknown",
                "review_status": row["review_status"]
                if row["review_status"] in {"geprueft", "nicht_geprueft"}
                else "unknown",
                "content_status": content_status,
                "original_status": original_status,
                "processing_quality": safe_metadata_text(
                    row["processing_quality"], 40, allow_empty=True
                ),
                "size_kb": int(row["groessekbytes"])
                if isinstance(row["groessekbytes"], int)
                and 0 <= row["groessekbytes"] <= 2_000_000
                else None,
                "snippet": str(row["snippet"])[:RECORD_MAX_SNIPPET_CHARS]
                if row["snippet"]
                else None,
            }
        )
    next_cursor = None
    if has_more and result:
        last_row = rows[page_size - 1]
        if search:
            cursor_key: str | float = float(last_row["search_rank"])
        else:
            cursor_column = _RECORD_SORT_SPECS[sort][0]
            cursor_key = str(last_row[cursor_column] or "")
        next_cursor = _encode_record_cursor(
            {"b": binding, "k": cursor_key, "id": int(last_row["id"])}
        )
    return {
        "documents": result,
        "next_cursor": next_cursor,
        "truncated": has_more,
        "total": None,
    }

def _fts_tokens(raw: str) -> list[str]:
    clean = " ".join(raw.split())
    if not 2 <= len(clean) <= 80:
        raise APIError(400, "query_length_invalid")
    tokens = re.findall(r"[A-Za-zÀ-ÿ0-9]{2,32}", clean)
    if not tokens or len(tokens) > 8 or " ".join(tokens).casefold() != clean.casefold():
        raise APIError(400, "query_not_allowed")
    return tokens

def _fts_literal_query(tokens: list[str]) -> str:
    """Construct only quoted FTS phrases; user input never supplies FTS syntax."""
    return " AND ".join(f'"{token.replace(chr(34), chr(34) * 2)}"' for token in tokens)

def _resolve_record_document(
    connection: sqlite3.Connection, opaque: str, *, reviewed: bool = False
) -> sqlite3.Row:
    if not re.fullmatch(r"api-document-[a-f0-9]{24}", opaque):


The dispatcher already has an allowlist and authenticated server wrapper, so the nutrition endpoints will inherit the same browser-session or bearer-token gate.


sed -n '2230,2385p' scripts/health/dashboard_v5/read_api.py


Output:
Chunk ID: 300f95
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1465
Output:
                "medications": sum(
                    len(medications.get(kind, []))
                    for kind in ("planned", "administered", "missed", "corrected")
                ),
                "symptoms": symptoms,
                "appointments": appointments,
                "documents": documents,
            }.items()
            if name in selected
        },
        "medical_statement": "Dokumentierte Daten ohne Diagnose-, Kausalitäts- oder Therapieaussage.",
    }
    return payload

def dispatch_api(database: Path, path: str, query: str) -> dict[str, Any]:
    document_route = re.fullmatch(
        r"/api/v1/documents/(api-document-[a-f0-9]{24})(?:/(matches))?", path
    )
    allowed_record = {
        "/api/v1/record-summary",
        "/api/v1/record-labs",
        "/api/v1/medications",
        "/api/v1/appointments",
        "/api/v1/documents",
        "/api/v1/doctor-report",
    }
    if (
        path not in API_PATHS
        and path not in allowed_record
        and not document_route
        and not re.fullmatch(r"/api/v1/day/\d{4}-\d{2}-\d{2}", path)
    ):
        raise APIError(404, "endpoint_not_found")
    connection = connect_read_only(database)
    try:
        if path == "/api/v1/record-summary":
            parse_query(query, set())
            return _record_summary(connection)
        if path == "/api/v1/record-labs":
            return _record_labs(connection, parse_query(query, {"q", "from", "to"}))
        if path == "/api/v1/medications":
            return _record_medications(connection, parse_query(query, {"from", "to"}))
        if path == "/api/v1/appointments":
            return _record_appointments(
                connection, parse_query(query, {"from", "to", "order", "institution"})
            )
        if path == "/api/v1/documents":
            return _record_document_rows(
                connection,
                parse_query(
                    query,
                    {
                        "from",
                        "to",
                        "category",
                        "institution",
                        "type",
                        "review_status",
                        "sort",
                        "q",
                        "cursor",
                        "limit",
                    },
                ),
            )
        if document_route:
            opaque, suffix = document_route.groups()
            if suffix == "matches":
                return _record_document_matches(
                    connection, opaque, parse_query(query, {"q"})
                )
            return _record_document_detail(
                connection,
                opaque,
                parse_query(query, {"cursor", "limit"}),
            )
        if path == "/api/v1/doctor-report":
            return _doctor_report(
                connection, parse_query(query, {"from", "to", "sections"})
            )
        if path == "/api/v1/metric-catalog":
            params = parse_query(query, {"q"})
            query_text = params.get("q", "")
            metrics = (
                catalog_search(connection, query_text)
                if query_text
                else public_catalog(connection)
            )
            inventory = inventory_sources(connection)
            return {
                "version": 2,
                "timezone": TZ_NAME,
                "today": local_today().isoformat(),
                "groups": {"metrics": metrics},
                "inventory": {
                    "apple_observed_identifiers": len(inventory["apple_health"]),
                    "apple_released_identifiers": sum(
                        item["status"] == "released"
                        for item in inventory["apple_health"]
                    ),
                    "canonical_verified_laboratory_parameters": len(
                        inventory["laboratory"]
                    ),
                    "observed_vital_sign_parameters": len(inventory["vital_signs"]),
                },
            }
        if path == "/api/v1/series":
            params = parse_query(query, {"metric", "from", "to", "resolution"})
            if not params.get("metric"):
                raise APIError(400, "metric_required")
            return _series(connection, params)
        if path.startswith("/api/v1/day/"):
            if query:
                raise APIError(400, "unknown_parameter")
            requested_day = parse_iso_day(path.rsplit("/", 1)[-1])
            if requested_day > local_today() + timedelta(days=MAX_FUTURE_DAYS):
                raise APIError(422, "future_date_not_allowed")
            return _day(connection, requested_day)
        if path == "/api/v1/calendar":
            params = parse_query(query, {"from", "to"})
            return _calendar(connection, params)
        if path == "/api/v1/events":
            params = parse_query(query, {"from", "to", "types"})
            return _events(connection, params)
        if path == "/api/v1/labs":
            parse_query(query, set())
            rows = _verified_lab_rows(connection)
            if len(rows) > MAX_LAB_ROWS:
                raise APIError(422, "row_limit_exceeded")
            return {
                "timezone": TZ_NAME,
                "reference_policy": "observation_specific_verified_original",
                "observations": rows,
            }
        if path == "/api/v1/search":
            params = parse_query(query, {"q"})
            return _search(connection, params.get("q", ""))
        raise APIError(404, "endpoint_not_found")
    except SourceInventoryLimitError as error:
        raise APIError(422, "source_inventory_limit_exceeded") from error
    except sqlite3.Error as error:
        raise APIError(503, "data_unavailable") from error
    finally:
        connection.close()


I found one more constraint: old V5 code still has legacy Explorer/correlation Chart.js branches, but the health-record-6e shell uses the newer unified Explorer. I’ll keep the fallback code dormant for non-record profiles and make the record profile ECharts-only.


Input:
*** Begin Patch
*** Update File: scripts/health/dashboard_v5/render.py
@@
-    explorer_scripts = (
-        ""
-        ""
-        if explorer_6c else ""
-    )
+    echarts_scripts = (
+        ""
+        ""
+        if explorer_6c else ""
+    )
+    explorer_scripts = "" if explorer_6c else ""
@@
-

Persönliche Trends
Cockpit

Dokumentierte Werte im persönlichen Verlauf; keine generische medizinische Bewertung.

Verlauf
Gesundheitsverlauf

<canvas id='cockpit-chart' role='img' aria-label='Gesundheitsverlauf'></canvas>
Keine dokumentierten Beobachtungen im gewählten Zeitraum.

+

Persönliche Trends
Cockpit

Dokumentierte Werte im persönlichen Verlauf; keine generische medizinische Bewertung.

Verlauf
Gesundheitsverlauf

Keine dokumentierten Beobachtungen im gewählten Zeitraum.

Zugängliche Datentabelle

@@
-

Ernährung

SIGHi-Zuordnungen dokumentieren Quellen und Unsicherheit; sie sind keine allgemeine Lebensmittelsicherheitsbewertung.

<canvas id='nutrition-chart' role='img' aria-label='Dokumentierter Histamin-Score'></canvas>
Keine vollständig klassifizierten Ernährungstage im gewählten Zeitraum.

Offene Lebensmittel-Zuordnungen

Nur Anzeige – Bearbeitung folgt im Ernährungs-Sprint 6G.

+

Ernährung

SIGHi-Zuordnungen dokumentieren Quellen und Unsicherheit; sie sind keine allgemeine Lebensmittelsicherheitsbewertung.

Keine vollständig klassifizierten Ernährungstage im gewählten Zeitraum.
Zugängliche Datentabelle

Ernährungstag
Datum <input type='date' data-nutrition-day-date><button type='button' data-nutrition-day-open>Tag laden</button>

Histamin und Aphthen

Neutrale Hypothesenansicht: gemeinsam dokumentierte Werte werden transparent gegenübergestellt; unvollständige Eingaben ergeben keinen Index.

Keine vollständigen Datenpaare im gewählten Zeitraum.

Offene Lebensmittel-Zuordnungen

Validierte Aktionen werden lokal vorgemerkt und erst vom privaten Worker angewendet.

@@
-{range_scripts}{explorer_scripts}{calendar_scripts}{record_scripts}{global_search_scripts}{prototype_scripts}
+{'' if explorer_6c else ""}{echarts_scripts}{range_scripts}{explorer_scripts}{calendar_scripts}{record_scripts}{global_search_scripts}{prototype_scripts}
*** End Patch


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

@@ -60,7 +60,8 @@
     )
-    explorer_scripts = (
+    echarts_scripts = (
         ""
-        ""
+        ""
         if explorer_6c else ""
     )
+    explorer_scripts = "" if explorer_6c else ""
     calendar_header = (
@@ -134,3 +135,3 @@

-

Persönliche Trends
Cockpit

Dokumentierte Werte im persönlichen Verlauf; keine generische medizinische Bewertung.

Verlauf
Gesundheitsverlauf

<canvas id='cockpit-chart' role='img' aria-label='Gesundheitsverlauf'></canvas>
Keine dokumentierten Beobachtungen im gewählten Zeitraum.

+

Persönliche Trends
Cockpit

Dokumentierte Werte im persönlichen Verlauf; keine generische medizinische Bewertung.

Verlauf
Gesundheitsverlauf

Keine dokumentierten Beobachtungen im gewählten Zeitraum.

Zugängliche Datentabelle

@@ -154,3 +155,3 @@
 {calendar_markup}
-

Ernährung

SIGHi-Zuordnungen dokumentieren Quellen und Unsicherheit; sie sind keine allgemeine Lebensmittelsicherheitsbewertung.

<canvas id='nutrition-chart' role='img' aria-label='Dokumentierter Histamin-Score'></canvas>
Keine vollständig klassifizierten Ernährungstage im gewählten Zeitraum.

Offene Lebensmittel-Zuordnungen

Nur Anzeige – Bearbeitung folgt im Ernährungs-Sprint 6G.

+

Ernährung

SIGHi-Zuordnungen dokumentieren Quellen und Unsicherheit; sie sind keine allgemeine Lebensmittelsicherheitsbewertung.

Keine vollständig klassifizierten Ernährungstage im gewählten Zeitraum.
Zugängliche Datentabelle

Ernährungstag
Datum <input type='date' data-nutrition-day-date><button type='button' data-nutrition-day-open>Tag laden</button>

Histamin und Aphthen

Neutrale Hypothesenansicht: gemeinsam dokumentierte Werte werden transparent gegenübergestellt; unvollständige Eingaben ergeben keinen Index.

Keine vollständigen Datenpaare im gewählten Zeitraum.

Offene Lebensmittel-Zuordnungen

Validierte Aktionen werden lokal vorgemerkt und erst vom privaten Worker angewendet.

 {record_markup}
@@ -159,3 +160,3 @@

-{range_scripts}{explorer_scripts}{calendar_scripts}{record_scripts}{global_search_scripts}{prototype_scripts}
+{'' if explorer_6c else ""}{echarts_scripts}{range_scripts}{explorer_scripts}{calendar_scripts}{record_scripts}{global_search_scripts}{prototype_scripts}
 Dashboard v5 benötigt lokales JavaScript. Dashboard v4 bleibt als Fallback verfügbar.


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


The V5 renderer now emits no Chart.js script for the health-record profile and includes local ECharts plus a shared helper once before V5 UI code. Next I’m adding that helper and wiring the existing controller to use it.


Input:
*** Begin Patch
*** Add File: scripts/health/assets/health-assets/dashboard-v5-echarts.js
+(() => {
+  "use strict";
+
+  const instances = new Map();
+  const palette = ["#235b68", "#7a5a22", "#5c5484", "#4f6b4f", "#8b3f58", "#27617a"];
+  const isoDay = value => String(value || "").slice(0, 10);
+
+  function formatDay(day) {
+    const date = new Date(`${day}T00:00:00`);
+    return Number.isNaN(date.getTime()) ? day : new Intl.DateTimeFormat("de-CH").format(date);
+  }
+
+  function hasECharts() {
+    return Boolean(window.echarts?.init);
+  }
+
+  function dispose(element) {
+    const chart = instances.get(element) || window.echarts?.getInstanceByDom?.(element);
+    if (chart) chart.dispose();
+    instances.delete(element);
+  }
+
+  function ensure(element) {
+    if (!element || !hasECharts()) return null;
+    const existing = instances.get(element) || window.echarts.getInstanceByDom(element);
+    if (existing) return existing;
+    const chart = window.echarts.init(element, null, { renderer: "canvas" });
+    instances.set(element, chart);
+    return chart;
+  }
+
+  function makeSeries({ metrics, rowsByMetric, days, mode = "raw", includeBaseline = false }) {
+    const series = [];
+    metrics.forEach((metric, index) => {
+      const rows = rowsByMetric.get(metric.id) || [];
+      const values = new Map(rows.map(point => [point.date, point.value]));
+      const suffix = mode === "baseline" ? " · Index 100" : metric.unit ? ` · ${metric.unit}` : "";
+      series.push({
+        id: metric.id,
+        name: `${metric.label || metric.id}${suffix}`,
+        type: "line",
+        connectNulls: false,
+        showSymbol: days.length <= 45,
+        symbolSize: 6,
+        lineStyle: { width: 2 },
+        emphasis: { focus: "series" },
+        data: days.map(day => [day, values.has(day) ? values.get(day) : null]),
+      });
+      if (includeBaseline && mode === "raw" && metrics.length === 1) {
+        const baseline = new Map(rows.map(point => [point.date, point.baseline]));
+        series.push({
+          id: `${metric.id}.baseline`,
+          name: `Persönliche ${metric.baseline_days || ""}-Tage-Basis`,
+          type: "line",
+          connectNulls: false,
+          showSymbol: false,
+          lineStyle: { width: 2, type: "dashed" },
+          data: days.map(day => [day, baseline.has(day) ? baseline.get(day) : null]),
+        });
+      }
+    });
+    return series;
+  }
+
+  function renderLine(element, options) {
+    const chart = ensure(element);
+    if (!chart) return null;
+    const units = options.mode === "baseline"
+      ? ["Index 100"]
+      : [...new Set((options.metrics || []).map(metric => metric.unit || "Wert"))];
+    const series = makeSeries(options);
+    chart.setOption({
+      animation: false,
+      color: palette,
+      aria: { enabled: true, description: options.aria || "Dokumentierte Werte; fehlende Tage bleiben Lücken." },
+      tooltip: {
+        trigger: "axis",
+        confine: true,
+        valueFormatter: value => value === null || value === undefined ? "unbekannt" : String(value),
+      },
+      legend: { type: "scroll", bottom: 0 },
+      grid: { left: 58, right: units.length > 1 ? 58 : 24, top: 28, bottom: 62, containLabel: true },
+      xAxis: { type: "time", name: "Datum", axisLabel: { formatter: value => formatDay(isoDay(value)) } },
+      yAxis: units.map((unit, index) => ({ type: "value", name: unit, scale: true, position: index ? "right" : "left" })),
+      series,
+    }, { replaceMerge: ["series", "yAxis"] });
+    chart.off("click");
+    chart.on("click", event => {
+      const day = Array.isArray(event?.value) ? isoDay(event.value[0]) : "";
+      if (/^\d{4}-\d{2}-\d{2}$/.test(day)) {
+        window.healthDayRouter?.open?.({ date: day, source: options.source || "echarts" });
+      }
+    });
+    return chart;
+  }
+
+  function renderTable(table, { metrics, rowsByMetric, days, mode = "raw" }) {
+    if (!table) return;
+    table.replaceChildren();
+    const head = document.createElement("thead");
+    const header = document.createElement("tr");
+    ["Datum", ...(metrics || []).map(metric => metric.label || metric.id)].forEach(label => {
+      const th = document.createElement("th");
+      th.textContent = label;
+      header.append(th);
+    });
+    head.append(header);
+    const body = document.createElement("tbody");
+    days.forEach(day => {
+      const row = document.createElement("tr");
+      const th = document.createElement("th");
+      const button = document.createElement("button");
+      button.type = "button";
+      button.textContent = day;
+      button.addEventListener("click", () => window.healthDayRouter?.open?.({ date: day, source: "echarts_table" }));
+      th.append(button);
+      row.append(th);
+      metrics.forEach(metric => {
+        const values = new Map((rowsByMetric.get(metric.id) || []).map(point => [point.date, point.value]));
+        const td = document.createElement("td");
+        const value = values.get(day);
+        td.textContent = value === null || value === undefined ? "unbekannt" : String(mode === "baseline" ? Math.round(value * 10) / 10 : value);
+        row.append(td);
+      });
+      body.append(row);
+    });
+    table.append(head, body);
+  }
+
+  function resizeAll() {
+    instances.forEach(chart => chart.resize());
+  }
+
+  window.HealthV5ECharts = { dispose, renderLine, renderTable, resizeAll, hasECharts };
+  window.addEventListener("resize", resizeAll);
+})();
*** End Patch


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

(() => {
  "use strict";

  const instances = new Map();
  const palette = ["#235b68", "#7a5a22", "#5c5484", "#4f6b4f", "#8b3f58", "#27617a"];
  const isoDay = value => String(value || "").slice(0, 10);

  function formatDay(day) {
    const date = new Date(`${day}T00:00:00`);
    return Number.isNaN(date.getTime()) ? day : new Intl.DateTimeFormat("de-CH").format(date);
  }

  function hasECharts() {
    return Boolean(window.echarts?.init);
  }

  function dispose(element) {
    const chart = instances.get(element) || window.echarts?.getInstanceByDom?.(element);
    if (chart) chart.dispose();
    instances.delete(element);
  }

  function ensure(element) {
    if (!element || !hasECharts()) return null;
    const existing = instances.get(element) || window.echarts.getInstanceByDom(element);
    if (existing) return existing;
    const chart = window.echarts.init(element, null, { renderer: "canvas" });
    instances.set(element, chart);
    return chart;
  }

  function makeSeries({ metrics, rowsByMetric, days, mode = "raw", includeBaseline = false }) {
    const series = [];
    metrics.forEach((metric, index) => {
      const rows = rowsByMetric.get(metric.id) || [];
      const values = new Map(rows.map(point => [point.date, point.value]));
      const suffix = mode === "baseline" ? " · Index 100" : metric.unit ? ` · ${metric.unit}` : "";
      series.push({
        id: metric.id,
        name: `${metric.label || metric.id}${suffix}`,
        type: "line",
        connectNulls: false,
        showSymbol: days.length <= 45,
        symbolSize: 6,
        lineStyle: { width: 2 },
        emphasis: { focus: "series" },
        data: days.map(day => [day, values.has(day) ? values.get(day) : null]),
      });
      if (includeBaseline && mode === "raw" && metrics.length === 1) {
        const baseline = new Map(rows.map(point => [point.date, point.baseline]));
        series.push({
          id: `${metric.id}.baseline`,
          name: `Persönliche ${metric.baseline_days || ""}-Tage-Basis`,
          type: "line",
          connectNulls: false,
          showSymbol: false,
          lineStyle: { width: 2, type: "dashed" },
          data: days.map(day => [day, baseline.has(day) ? baseline.get(day) : null]),
        });
      }
    });
    return series;
  }

  function renderLine(element, options) {
    const chart = ensure(element);
    if (!chart) return null;
    const units = options.mode === "baseline"
      ? ["Index 100"]
      : [...new Set((options.metrics || []).map(metric => metric.unit || "Wert"))];
    const series = makeSeries(options);
    chart.setOption({
      animation: false,
      color: palette,
      aria: { enabled: true, description: options.aria || "Dokumentierte Werte; fehlende Tage bleiben Lücken." },
      tooltip: {
        trigger: "axis",
        confine: true,
        valueFormatter: value => value === null || value === undefined ? "unbekannt" : String(value),
      },
      legend: { type: "scroll", bottom: 0 },
      grid: { left: 58, right: units.length > 1 ? 58 : 24, top: 28, bottom: 62, containLabel: true },
      xAxis: { type: "time", name: "Datum", axisLabel: { formatter: value => formatDay(isoDay(value)) } },
      yAxis: units.map((unit, index) => ({ type: "value", name: unit, scale: true, position: index ? "right" : "left" })),
      series,
    }, { replaceMerge: ["series", "yAxis"] });
    chart.off("click");
    chart.on("click", event => {
      const day = Array.isArray(event?.value) ? isoDay(event.value[0]) : "";
      if (/^\d{4}-\d{2}-\d{2}$/.test(day)) {
        window.healthDayRouter?.open?.({ date: day, source: options.source || "echarts" });
      }
    });
    return chart;
  }

  function renderTable(table, { metrics, rowsByMetric, days, mode = "raw" }) {
    if (!table) return;
    table.replaceChildren();
    const head = document.createElement("thead");
    const header = document.createElement("tr");
    ["Datum", ...(metrics || []).map(metric => metric.label || metric.id)].forEach(label => {
      const th = document.createElement("th");
      th.textContent = label;
      header.append(th);
    });
    head.append(header);
    const body = document.createElement("tbody");
    days.forEach(day => {
      const row = document.createElement("tr");
      const th = document.createElement("th");
      const button = document.createElement("button");
      button.type = "button";
      button.textContent = day;
      button.addEventListener("click", () => window.healthDayRouter?.open?.({ date: day, source: "echarts_table" }));
      th.append(button);
      row.append(th);
      metrics.forEach(metric => {
        const values = new Map((rowsByMetric.get(metric.id) || []).map(point => [point.date, point.value]));
        const td = document.createElement("td");
        const value = values.get(day);
        td.textContent = value === null || value === undefined ? "unbekannt" : String(mode === "baseline" ? Math.round(value * 10) / 10 : value);
        row.append(td);
      });
      body.append(row);
    });
    table.append(head, body);
  }

  function resizeAll() {
    instances.forEach(chart => chart.resize());
  }

  window.HealthV5ECharts = { dispose, renderLine, renderTable, resizeAll, hasECharts };
  window.addEventListener("resize", resizeAll);
})();


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


The shared helper owns ECharts initialization/disposal, point-click day routing, and accessible table rendering. I’m replacing visible SVG sparklines with tiny ECharts cards now.


Input:
*** Begin Patch
*** Update File: scripts/health/health_dashboard_server.py
@@
     "/health-assets/dashboard-v5-api-explorer.js": (
         "dashboard-v5-api-explorer.js",
         "text/javascript; charset=utf-8",
     ),
+    "/health-assets/dashboard-v5-echarts.js": (
+        "dashboard-v5-echarts.js",
+        "text/javascript; charset=utf-8",
+    ),
*** End Patch


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

@@ -81,2 +81,6 @@
     ),
+    "/health-assets/dashboard-v5-echarts.js": (
+        "dashboard-v5-echarts.js",
+        "text/javascript; charset=utf-8",
+    ),
     "/health-assets/dashboard-v5-day-controller.js": (


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