diff --git a/k8s/family/duckdb-backend.yaml b/k8s/family/duckdb-backend.yaml
new file mode 100644
index 0000000..9a19cc3
--- /dev/null
+++ b/k8s/family/duckdb-backend.yaml
@@ -0,0 +1,151 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: duckdb-backend
+ namespace: family
+ labels:
+ app.kubernetes.io/name: duckdb-backend
+ app.kubernetes.io/part-of: family-portal
+spec:
+ replicas: 1
+ strategy:
+ type: Recreate
+ selector:
+ matchLabels:
+ app.kubernetes.io/name: duckdb-backend
+ template:
+ metadata:
+ labels:
+ app.kubernetes.io/name: duckdb-backend
+ app.kubernetes.io/part-of: family-portal
+ spec:
+ imagePullSecrets:
+ - name: registry-haumdaucher-de
+ initContainers:
+ - name: git-init
+ image: alpine/git:latest
+ command: ["/bin/sh", "-c"]
+ args:
+ - |
+ set -e
+ export HOME=/tmp
+ cd /wiki
+ if [ ! -d ".git" ]; then
+ echo "Initializing wiki repository on dedicated PVC..."
+ REPO_URL="https://${GIT_SYNC_USERNAME}:${GIT_SYNC_PASSWORD}@git.moritzgraf.de/moritz/knowledge-wiki.git"
+ git config --global http.sslVerify false
+ git config --global --add safe.directory /wiki
+ git config --global --add safe.directory '*'
+ git init
+ git remote add origin "$REPO_URL" || git remote set-url origin "$REPO_URL"
+ git fetch origin main
+ git checkout -f -B main origin/main
+ echo "Clone completed successfully."
+ else
+ echo "Repository already initialized on PVC, updating..."
+ REPO_URL="https://${GIT_SYNC_USERNAME}:${GIT_SYNC_PASSWORD}@git.moritzgraf.de/moritz/knowledge-wiki.git"
+ git config --global http.sslVerify false
+ git config --global --add safe.directory /wiki
+ git config --global --add safe.directory '*'
+ git remote set-url origin "$REPO_URL"
+ if [ -f .git/shallow ]; then
+ echo "Unshallowing existing repository..."
+ git fetch --unshallow origin main || git fetch origin main
+ else
+ git fetch origin main
+ fi
+ git pull --rebase --autostash origin main || true
+ fi
+ env:
+ - name: GIT_SYNC_USERNAME
+ valueFrom:
+ secretKeyRef:
+ name: wiki-git-credentials
+ key: git-sync-username
+ - name: GIT_SYNC_PASSWORD
+ valueFrom:
+ secretKeyRef:
+ name: wiki-git-credentials
+ key: wiki-sync-token
+ volumeMounts:
+ - name: duckdb-wiki-data
+ mountPath: /wiki
+ containers:
+ - name: duckdb-backend
+ image: registry.haumdaucher.de/family/duckdb-backend:latest
+ imagePullPolicy: Always
+ env:
+ - name: DATA_DIR
+ value: "/wiki/data"
+ - name: DUCKDB_DATA_DIR
+ value: "/wiki/data"
+ - name: WIKI_DIR
+ value: "/wiki"
+ - name: GIT_REPO_DIR
+ value: "/wiki"
+ - name: GIT_REPO_URL
+ value: "https://git.moritzgraf.de/moritz/knowledge-wiki.git"
+ - name: GIT_REMOTE_URL
+ value: "https://git.moritzgraf.de/moritz/knowledge-wiki.git"
+ - name: GIT_SYNC_USERNAME
+ valueFrom:
+ secretKeyRef:
+ name: wiki-git-credentials
+ key: git-sync-username
+ - name: GIT_SYNC_PASSWORD
+ valueFrom:
+ secretKeyRef:
+ name: wiki-git-credentials
+ key: wiki-sync-token
+ - name: GIT_SYNC_INTERVAL_SECONDS
+ value: "60"
+ - name: PORT
+ value: "8000"
+ - name: HOST
+ value: "0.0.0.0"
+ ports:
+ - name: http
+ containerPort: 8000
+ volumeMounts:
+ - name: duckdb-wiki-data
+ mountPath: /wiki
+ resources:
+ requests:
+ cpu: 50m
+ memory: 128Mi
+ limits:
+ cpu: 1000m
+ memory: 1Gi
+ readinessProbe:
+ httpGet:
+ path: /duckdb-api/v1/healthz
+ port: 8000
+ initialDelaySeconds: 5
+ periodSeconds: 10
+ livenessProbe:
+ httpGet:
+ path: /duckdb-api/v1/healthz
+ port: 8000
+ initialDelaySeconds: 15
+ periodSeconds: 20
+ volumes:
+ - name: duckdb-wiki-data
+ persistentVolumeClaim:
+ claimName: duckdb-wiki-data
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: duckdb-backend
+ namespace: family
+ labels:
+ app.kubernetes.io/name: duckdb-backend
+ app.kubernetes.io/part-of: family-portal
+spec:
+ type: ClusterIP
+ ports:
+ - port: 8000
+ targetPort: 8000
+ name: http
+ selector:
+ app.kubernetes.io/name: duckdb-backend
diff --git a/k8s/family/git-credentials.secret.yaml b/k8s/family/git-credentials.secret.yaml
new file mode 100644
index 0000000..18b6048
Binary files /dev/null and b/k8s/family/git-credentials.secret.yaml differ
diff --git a/k8s/family/ingress.yaml b/k8s/family/ingress.yaml
new file mode 100644
index 0000000..62c4535
--- /dev/null
+++ b/k8s/family/ingress.yaml
@@ -0,0 +1,55 @@
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: family-portal
+ namespace: family
+ annotations:
+ kubernetes.io/ingress.class: "traefik"
+ cert-manager.io/cluster-issuer: "letsencrypt-prod"
+ kubernetes.io/tls-acme: "true"
+ traefik.ingress.kubernetes.io/router.middlewares: auth-authentik-forwardauth@kubernetescrd
+spec:
+ ingressClassName: traefik
+ tls:
+ - hosts:
+ - family.haumdaucher.de
+ secretName: family-haumdaucher-de-tls
+ rules:
+ - host: family.haumdaucher.de
+ http:
+ paths:
+ - path: /duckdb-api
+ pathType: Prefix
+ backend:
+ service:
+ name: duckdb-backend
+ port:
+ number: 8000
+ - path: /tracking/mila
+ pathType: Prefix
+ backend:
+ service:
+ name: mila
+ port:
+ number: 80
+ - path: /tracking/moritz
+ pathType: Prefix
+ backend:
+ service:
+ name: moritz
+ port:
+ number: 80
+ - path: /outpost.goauthentik.io
+ pathType: Prefix
+ backend:
+ service:
+ name: duckdb-backend
+ port:
+ number: 8000
+ - path: /
+ pathType: Prefix
+ backend:
+ service:
+ name: mila
+ port:
+ number: 80
diff --git a/k8s/family/mila.yaml b/k8s/family/mila.yaml
new file mode 100644
index 0000000..3fd6707
--- /dev/null
+++ b/k8s/family/mila.yaml
@@ -0,0 +1,64 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: mila
+ namespace: family
+ labels:
+ app.kubernetes.io/name: mila
+ app.kubernetes.io/part-of: family-portal
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app.kubernetes.io/name: mila
+ template:
+ metadata:
+ labels:
+ app.kubernetes.io/name: mila
+ app.kubernetes.io/part-of: family-portal
+ spec:
+ imagePullSecrets:
+ - name: registry-haumdaucher-de
+ containers:
+ - name: mila
+ image: registry.haumdaucher.de/family/mila:latest
+ imagePullPolicy: Always
+ ports:
+ - name: http
+ containerPort: 80
+ resources:
+ requests:
+ cpu: 25m
+ memory: 32Mi
+ limits:
+ cpu: 100m
+ memory: 128Mi
+ readinessProbe:
+ httpGet:
+ path: /tracking/mila/
+ port: 80
+ initialDelaySeconds: 5
+ periodSeconds: 10
+ livenessProbe:
+ httpGet:
+ path: /tracking/mila/
+ port: 80
+ initialDelaySeconds: 10
+ periodSeconds: 15
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: mila
+ namespace: family
+ labels:
+ app.kubernetes.io/name: mila
+ app.kubernetes.io/part-of: family-portal
+spec:
+ type: ClusterIP
+ ports:
+ - port: 80
+ targetPort: 80
+ name: http
+ selector:
+ app.kubernetes.io/name: mila
diff --git a/k8s/family/moritz.yaml b/k8s/family/moritz.yaml
new file mode 100644
index 0000000..9d8d70c
--- /dev/null
+++ b/k8s/family/moritz.yaml
@@ -0,0 +1,64 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: moritz
+ namespace: family
+ labels:
+ app.kubernetes.io/name: moritz
+ app.kubernetes.io/part-of: family-portal
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app.kubernetes.io/name: moritz
+ template:
+ metadata:
+ labels:
+ app.kubernetes.io/name: moritz
+ app.kubernetes.io/part-of: family-portal
+ spec:
+ imagePullSecrets:
+ - name: registry-haumdaucher-de
+ containers:
+ - name: moritz
+ image: registry.haumdaucher.de/family/moritz:latest
+ imagePullPolicy: Always
+ ports:
+ - name: http
+ containerPort: 80
+ resources:
+ requests:
+ cpu: 25m
+ memory: 32Mi
+ limits:
+ cpu: 100m
+ memory: 128Mi
+ readinessProbe:
+ httpGet:
+ path: /tracking/moritz/
+ port: 80
+ initialDelaySeconds: 5
+ periodSeconds: 10
+ livenessProbe:
+ httpGet:
+ path: /tracking/moritz/
+ port: 80
+ initialDelaySeconds: 10
+ periodSeconds: 15
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: moritz
+ namespace: family
+ labels:
+ app.kubernetes.io/name: moritz
+ app.kubernetes.io/part-of: family-portal
+spec:
+ type: ClusterIP
+ ports:
+ - port: 80
+ targetPort: 80
+ name: http
+ selector:
+ app.kubernetes.io/name: moritz
diff --git a/k8s/family/namespace.yaml b/k8s/family/namespace.yaml
new file mode 100644
index 0000000..64f8947
--- /dev/null
+++ b/k8s/family/namespace.yaml
@@ -0,0 +1,6 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: family
+ labels:
+ name: family
diff --git a/k8s/family/pvc.yaml b/k8s/family/pvc.yaml
new file mode 100644
index 0000000..a7b146b
--- /dev/null
+++ b/k8s/family/pvc.yaml
@@ -0,0 +1,15 @@
+apiVersion: v1
+kind: PersistentVolumeClaim
+metadata:
+ name: duckdb-wiki-data
+ namespace: family
+ labels:
+ app.kubernetes.io/name: duckdb-backend
+ app.kubernetes.io/part-of: family-portal
+spec:
+ accessModes:
+ - ReadWriteOnce
+ storageClassName: openebs-hostpath
+ resources:
+ requests:
+ storage: 5Gi
diff --git a/k8s/family/registry.secret.yaml b/k8s/family/registry.secret.yaml
new file mode 100644
index 0000000..e7587d8
Binary files /dev/null and b/k8s/family/registry.secret.yaml differ
diff --git a/k8s/minio/hosting-ingress.yaml b/k8s/minio/hosting-ingress.yaml
index 5ff9fee..bffe658 100644
--- a/k8s/minio/hosting-ingress.yaml
+++ b/k8s/minio/hosting-ingress.yaml
@@ -10,15 +10,37 @@ spec:
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
- name: hosting-legacy-redirect
+ name: hosting-redirect-mila
namespace: minio
spec:
redirectRegex:
- regex: "^https?://([^/]+)/(mila|moritz|nutrition|sleep)\\.html(.*)"
- replacement: "https://${1}/family/${2}.html${3}"
- permanent: false
+ regex: "^https?://([^/]+)/(?:family/)?(mila|sleep)\\.html.*"
+ replacement: "https://family.haumdaucher.de/tracking/mila/"
+ permanent: true
---
-# Public Ingress for hosting.haumdaucher.de (Recipes, Public Assets)
+apiVersion: traefik.io/v1alpha1
+kind: Middleware
+metadata:
+ name: hosting-redirect-moritz
+ namespace: minio
+spec:
+ redirectRegex:
+ regex: "^https?://([^/]+)/(?:family/)?(moritz|nutrition)\\.html.*"
+ replacement: "https://family.haumdaucher.de/tracking/moritz/"
+ permanent: true
+---
+apiVersion: traefik.io/v1alpha1
+kind: Middleware
+metadata:
+ name: hosting-redirect-family-root
+ namespace: minio
+spec:
+ redirectRegex:
+ regex: "^https?://([^/]+)/family/?$"
+ replacement: "https://family.haumdaucher.de/"
+ permanent: true
+---
+# Public Ingress for hosting.haumdaucher.de (Recipes, Public Assets, Legacy Redirects)
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
@@ -29,7 +51,7 @@ metadata:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
kubernetes.io/tls-acme: "true"
traefik.ingress.kubernetes.io/router.priority: "10"
- traefik.ingress.kubernetes.io/router.middlewares: minio-hosting-legacy-redirect@kubernetescrd,minio-hosting-add-prefix@kubernetescrd
+ traefik.ingress.kubernetes.io/router.middlewares: minio-hosting-redirect-mila@kubernetescrd,minio-hosting-redirect-moritz@kubernetescrd,minio-hosting-redirect-family-root@kubernetescrd,minio-hosting-add-prefix@kubernetescrd
spec:
ingressClassName: traefik
rules:
@@ -48,8 +70,7 @@ spec:
- hosting.haumdaucher.de
secretName: hosting-haumdaucher-de-tls
---
-# Protected Ingress for /family (Family Dashboards, Nutrition & Sleep Analytics)
-# Protected by Authentik ForwardAuth
+# Protected Ingress for /family (Redirects and Authentik Outpost)
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
@@ -60,7 +81,7 @@ metadata:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
kubernetes.io/tls-acme: "true"
traefik.ingress.kubernetes.io/router.priority: "100"
- traefik.ingress.kubernetes.io/router.middlewares: auth-authentik-forwardauth@kubernetescrd,minio-hosting-add-prefix@kubernetescrd
+ traefik.ingress.kubernetes.io/router.middlewares: minio-hosting-redirect-mila@kubernetescrd,minio-hosting-redirect-moritz@kubernetescrd,minio-hosting-redirect-family-root@kubernetescrd,auth-authentik-forwardauth@kubernetescrd,minio-hosting-add-prefix@kubernetescrd
spec:
ingressClassName: traefik
rules:
diff --git a/k8s/openclaw/cronjob-wiki-backup.secret.yaml b/k8s/openclaw/cronjob-wiki-backup.secret.yaml
index 9f223f2..f516948 100644
Binary files a/k8s/openclaw/cronjob-wiki-backup.secret.yaml and b/k8s/openclaw/cronjob-wiki-backup.secret.yaml differ
diff --git a/k8s/openclaw/crons.yaml b/k8s/openclaw/crons.yaml
index 886d9b8..497d02d 100644
--- a/k8s/openclaw/crons.yaml
+++ b/k8s/openclaw/crons.yaml
@@ -25,18 +25,7 @@ jobs:
delivery: "none"
command: "python3 /home/node/.openclaw/scripts/compact_sessions.py"
- # 2. Family Dashboards & Web Analytics
- - name: "dashboards-periodic-publish"
- declarationKey: "dashboards-periodic-publish"
- description: "Periodically regenerates and uploads Mila & Moritz family HTML dashboards to S3"
- schedule:
- kind: "every"
- every: "10m"
- session: "isolated"
- delivery: "none"
- command: "sh -lc 'export PATH=/home/node/.openclaw/bin:$PATH; cd /home/node/mop-skills/mop-skills/skills/datastore && uv run scripts/update_website.py --data-dir /home/node/.openclaw/wiki/main/data && uv run scripts/update_moritz_website.py --data-dir /home/node/.openclaw/wiki/main/data'"
-
- # 3. Nightly Garmin Connect Mirror
+ # 2. Nightly Garmin Connect Mirror
- name: "garmin-nightly-mirror"
declarationKey: "garmin-nightly-mirror"
description: "Nächtlicher deterministischer Spiegel der Garmin Connect Daten (Aktivitäten, Daily Summary, Schlaf) in Git JSONL"
@@ -56,13 +45,7 @@ jobs:
Vorgehen:
1. Führe den Garmin-Sync-Befehl aus:
uv run /home/node/mop-skills/mop-skills/skills/datastore/scripts/garmin_sync.py --days 2 --data-dir /home/node/.openclaw/wiki/main/data
- 2. Aktualisiere die gerenderten Markdown-Ansichten:
- PYTHONPATH=/home/node/mop-skills/mop-skills/skills/datastore/scripts uv run /home/node/mop-skills/mop-skills/skills/datastore/scripts/render_views.py --data-dir /home/node/.openclaw/wiki/main/data --wiki-dir /home/node/.openclaw/wiki/main
- 3. Aktualisiere das persönliche Health & Nutrition Dashboard (moritz.html):
- uv run /home/node/mop-skills/mop-skills/skills/datastore/scripts/update_moritz_website.py --data-dir /home/node/.openclaw/wiki/main/data
- 4. Git Commit & Push im Knowledge Wiki:
- cd /home/node/.openclaw/wiki/main && git add data/garmin/ entities/nutrition-log/ public/ && git diff --staged --quiet || (git commit -m "chore(garmin): automated nightly mirror [skip ci]" && git push origin main)
- 5. Finale Antwort:
+ 2. Finale Antwort:
- Bei Erfolg: Gib exakt 'NO_REPLY' aus (intentional silence, kein Telegram-Spam in der Nacht).
- Nur bei Authentifizierungsfehlern: Informiere kurz via Telegram (Thema #Sport), dass ein Garmin-Re-Auth nötig ist, oder starte den Re-Auth-Flow.
diff --git a/knowledge-wiki b/knowledge-wiki
index 1008b2c..419b006 160000
--- a/knowledge-wiki
+++ b/knowledge-wiki
@@ -1 +1 @@
-Subproject commit 1008b2c18dd1c1e4a7ff4bd6353fe5756a8cd8db
+Subproject commit 419b00657436708969a5f69fe00baee998fe9c48
diff --git a/projects/duckdb-backend/AGENTS.md b/projects/duckdb-backend/AGENTS.md
new file mode 100644
index 0000000..2e676d2
--- /dev/null
+++ b/projects/duckdb-backend/AGENTS.md
@@ -0,0 +1,13 @@
+# AGENTS.md — DuckDB Backend
+
+## Purpose
+FastAPI HTTP backend executing in-memory DuckDB queries against Git-backed JSONL data in the Knowledge Wiki.
+
+## Development & Testing
+```bash
+# Run tests
+uv run --project projects/duckdb-backend pytest -v projects/duckdb-backend/tests
+
+# Run locally
+uv run --project projects/duckdb-backend uvicorn app.main:app --port 8000
+```
diff --git a/projects/duckdb-backend/Dockerfile b/projects/duckdb-backend/Dockerfile
new file mode 100644
index 0000000..de34232
--- /dev/null
+++ b/projects/duckdb-backend/Dockerfile
@@ -0,0 +1,26 @@
+FROM python:3.12-slim
+
+# Install system dependencies (git for wiki sync)
+RUN apt-get update && apt-get install -y --no-install-recommends \
+ git \
+ ca-certificates \
+ && rm -rf /var/lib/apt/lists/*
+
+# Install uv for fast dependency resolution
+COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
+
+WORKDIR /app
+
+# Install dependencies
+COPY pyproject.toml .
+RUN uv pip install --system --no-cache -r pyproject.toml
+
+# Copy application source
+COPY app/ ./app/
+
+ENV PYTHONUNBUFFERED=1
+ENV PORT=8000
+ENV HOST=0.0.0.0
+EXPOSE 8000
+
+CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
diff --git a/projects/duckdb-backend/README.md b/projects/duckdb-backend/README.md
new file mode 100644
index 0000000..d5ca3d5
--- /dev/null
+++ b/projects/duckdb-backend/README.md
@@ -0,0 +1,26 @@
+# DuckDB Backend
+
+HTTP-based analytical and transactional backend for personal logging data stored in the Knowledge Wiki.
+Powered by Python 3.12, FastAPI, and in-memory DuckDB querying JSONL files directly.
+
+## Architecture
+
+- **Single Source of Truth**: Knowledge Wiki (`/wiki` git repository with data in `/wiki/data/**/*.jsonl`).
+- **Engine**: In-memory DuckDB reading `sleep`, `nutrition`, `mila`, `entities`, and `garmin` partitions.
+- **Git Synchronization**:
+ - Background periodic `git pull --rebase origin main` (every 60s).
+ - Mutating operations (`POST /duckdb-api/v1/events`) append to typed JSONL partition files, sort them, commit, rebase, and push to origin main.
+ - Conflicts trigger automatic rebase abort and flag `git_conflict: true` on `/status` and `/healthz`.
+
+## Endpoints
+
+- `GET /duckdb-api/v1/mila/dashboard`: Curated aggregated dashboard for Mila (sleep, wake timer, feedings, diapers, WHO percentiles).
+- `GET /duckdb-api/v1/moritz/dashboard`: Curated aggregated dashboard for Moritz (today's intake, yesterday's 24h deficit/surplus, Garmin recovery & sleep, 7d/30d trends).
+- `POST /duckdb-api/v1/query`: Generic read-only SQL queries against DuckDB tables and views.
+- `POST /duckdb-api/v1/events`: Record new events (sleep, nutrition, diaper, feeding, growth).
+- `POST /duckdb-api/v1/events/stop`: Close active open sleep session or interval.
+- `DELETE /duckdb-api/v1/events/{id}`: Soft-delete event.
+- `POST /duckdb-api/v1/entities`: Upsert entities (`foods`, `recipes`, `places`, `people`).
+- `GET /duckdb-api/v1/healthz`: Liveness and readiness probe.
+- `GET /duckdb-api/v1/status`: Git synchronization status and table row counts.
+- `POST /duckdb-api/v1/git/sync`: Manual pull or webhook sync trigger.
diff --git a/projects/duckdb-backend/app/__init__.py b/projects/duckdb-backend/app/__init__.py
new file mode 100644
index 0000000..b9d56a4
--- /dev/null
+++ b/projects/duckdb-backend/app/__init__.py
@@ -0,0 +1 @@
+# app package
diff --git a/projects/duckdb-backend/app/config.py b/projects/duckdb-backend/app/config.py
new file mode 100644
index 0000000..bbd72bc
--- /dev/null
+++ b/projects/duckdb-backend/app/config.py
@@ -0,0 +1,46 @@
+from __future__ import annotations
+
+import os
+from pathlib import Path
+
+def find_default_wiki_dir() -> Path:
+ env_wiki = os.environ.get("WIKI_DIR") or os.environ.get("GIT_REPO_DIR")
+ if env_wiki:
+ return Path(env_wiki).resolve()
+
+ candidates = [
+ Path("/wiki"),
+ Path.cwd() / "knowledge-wiki",
+ Path.cwd().parent.parent / "knowledge-wiki",
+ Path("/Users/moritz/src/infrapuzzle/knowledge-wiki"),
+ Path.home() / "src" / "infrapuzzle" / "knowledge-wiki",
+ Path("/home/node/.openclaw/wiki/main"),
+ ]
+ for c in candidates:
+ if c.is_dir() and (c / "data").is_dir():
+ return c.resolve()
+
+ return (Path.cwd() / "knowledge-wiki").resolve()
+
+
+WIKI_DIR = find_default_wiki_dir()
+DATA_DIR = Path(
+ os.environ.get("DATA_DIR")
+ or os.environ.get("DUCKDB_DATA_DIR")
+ or (WIKI_DIR / "data")
+).resolve()
+
+GIT_REPO_URL = os.environ.get(
+ "GIT_REPO_URL",
+ os.environ.get("GIT_REMOTE_URL", "https://git.moritzgraf.de/moritz/knowledge-wiki.git"),
+)
+GIT_USERNAME = os.environ.get("GIT_SYNC_USERNAME", "")
+GIT_PASSWORD = os.environ.get("GIT_SYNC_PASSWORD", os.environ.get("WIKI_SYNC_TOKEN", ""))
+GIT_BRANCH = os.environ.get("GIT_BRANCH", "main")
+GIT_SYNC_INTERVAL_SECONDS = int(os.environ.get("GIT_SYNC_INTERVAL_SECONDS", "60"))
+AUTO_GIT_SYNC = os.environ.get("AUTO_GIT_SYNC", "true").lower() in ("1", "true", "yes")
+GIT_AUTHOR_NAME = os.environ.get("GIT_AUTHOR_NAME", "DuckDB Backend")
+GIT_AUTHOR_EMAIL = os.environ.get("GIT_AUTHOR_EMAIL", "duckdb-backend@haumdaucher.de")
+
+PORT = int(os.environ.get("PORT", "8000"))
+HOST = os.environ.get("HOST", "0.0.0.0")
diff --git a/projects/duckdb-backend/app/engine.py b/projects/duckdb-backend/app/engine.py
new file mode 100644
index 0000000..f6ac132
--- /dev/null
+++ b/projects/duckdb-backend/app/engine.py
@@ -0,0 +1,2287 @@
+from __future__ import annotations
+
+import datetime
+import json
+import logging
+import math
+import os
+import re
+import threading
+import time
+from pathlib import Path
+from typing import Any, Dict, List, Optional, Tuple, Union
+from zoneinfo import ZoneInfo
+
+import duckdb
+from ulid import ULID
+
+from app.config import DATA_DIR
+from app.git_sync import GitSyncManager
+from app.models import EventCreateRequest, EventStopRequest, EntityPutRequest, GarminSyncRequest
+from app.who_growth import calculate_percentile
+
+logger = logging.getLogger("duckdb_backend.engine")
+BERLIN_TZ = ZoneInfo("Europe/Berlin")
+MILA_BIRTHDATE = datetime.date(2026, 3, 14)
+
+WEEKDAYS_DE = {0: "Mo", 1: "Di", 2: "Mi", 3: "Do", 4: "Fr", 5: "Sa", 6: "So"}
+WEEKDAYS_LONG_DE = {
+ 0: "Montag",
+ 1: "Dienstag",
+ 2: "Mittwoch",
+ 3: "Donnerstag",
+ 4: "Freitag",
+ 5: "Samstag",
+ 6: "Sonntag",
+}
+
+VALID_COLS_MAP = {
+ "nutrition": {
+ "id", "kind", "subject", "actor", "start", "end", "estimated", "meal",
+ "note", "payload", "source_ref", "created_at", "updated_at", "deleted_at"
+ },
+ "mila": {
+ "id", "kind", "subject", "actor", "start", "end", "note", "payload",
+ "source_ref", "created_at", "updated_at", "deleted_at"
+ },
+ "sleep": {
+ "id", "subject", "calendarDate", "sleepType", "location", "isOpen",
+ "startIso", "endIso", "sleepStartTimestampGMT", "sleepEndTimestampGMT",
+ "sleepStartTimestampLocal", "sleepEndTimestampLocal", "sleepTimeSeconds",
+ "napTimeSeconds", "deepSleepSeconds", "lightSleepSeconds", "remSleepSeconds",
+ "awakeSleepSeconds", "awakeCount", "avgSleepStress", "avgHeartRate",
+ "averageSpO2Value", "lowestSpO2Value", "highestSpO2Value",
+ "averageRespirationValue", "sleepScores", "avgOvernightHrv", "hrvStatus",
+ "bodyBatteryChange", "restingHeartRate", "restlessMomentsCount",
+ "note", "actor", "created_at", "updated_at", "deleted_at"
+ },
+}
+
+
+def parse_duration(val: str) -> datetime.timedelta:
+ s = val.strip().lower()
+ if not s:
+ raise ValueError("Empty duration string")
+
+ m_dec_h = re.fullmatch(r"^(\d+(?:\.\d+)?)\s*(?:h|stunden?)$", s)
+ if m_dec_h:
+ return datetime.timedelta(hours=float(m_dec_h.group(1)))
+
+ m_dec_m = re.fullmatch(r"^(\d+(?:\.\d+)?)\s*(?:m|min|minuten?)$", s)
+ if m_dec_m:
+ return datetime.timedelta(minutes=float(m_dec_m.group(1)))
+
+ m_comp = re.fullmatch(
+ r"^(?:(\d+)\s*(?:h|stunden?))?\s*(?:(\d+)\s*(?:m|min|minuten?))?\s*(?:(\d+)\s*(?:s|sek|sekunden?))?$",
+ s,
+ )
+ if m_comp and any(m_comp.groups()):
+ h = int(m_comp.group(1) or 0)
+ m = int(m_comp.group(2) or 0)
+ sec = int(m_comp.group(3) or 0)
+ return datetime.timedelta(hours=h, minutes=m, seconds=sec)
+
+ if s.isdigit():
+ return datetime.timedelta(minutes=int(s))
+
+ raise ValueError(f"Unrecognized duration format: '{val}'. Expected '15m', '1h', etc.")
+
+
+def calculate_age_str(as_of: datetime.date) -> str:
+ days = (as_of - MILA_BIRTHDATE).days
+ if days < 0:
+ return "Geboren am 14.03.2026"
+ months = days // 30.4375
+ weeks = (days % 30.4375) // 7
+ if months < 1:
+ return f"{days // 7} Wochen alt"
+ if weeks > 0:
+ return f"{int(months)} Monate, {int(weeks)} Wochen alt"
+ return f"{int(months)} Monate alt"
+
+
+class DuckDBEngine:
+ def __init__(self, data_dir: Optional[Path] = None, git_sync_manager: Optional[GitSyncManager] = None):
+ self.data_dir = (data_dir or DATA_DIR).resolve()
+ self.git_sync_manager = git_sync_manager
+ self._lock = threading.RLock()
+ self.con = duckdb.connect(":memory:")
+ self._init_schemas()
+ self.reload_all()
+
+ def _init_schemas(self):
+ with self._lock:
+ try:
+ self.con.execute("INSTALL icu; LOAD icu;")
+ self.con.execute("SET TimeZone = 'UTC';")
+ except Exception:
+ pass
+
+ self.con.execute("""
+ CREATE MACRO IF NOT EXISTS distance_m(lat1, lon1, lat2, lon2) AS
+ 6371000 * 2 * asin(sqrt(
+ sin(radians(lat2 - lat1) / 2) ^ 2 +
+ cos(radians(lat1)) * cos(radians(lat2)) * sin(radians(lon2 - lon1) / 2) ^ 2
+ ));
+ """)
+
+ schema_dir = self.data_dir / "_schema"
+ if not schema_dir.is_dir():
+ candidates = [
+ Path("/wiki/data/_schema"),
+ Path.cwd() / "knowledge-wiki" / "data" / "_schema",
+ Path.cwd().parent.parent / "knowledge-wiki" / "data" / "_schema",
+ ]
+ for c in candidates:
+ if c.is_dir():
+ schema_dir = c
+ break
+
+ if schema_dir.is_dir():
+ for f in ["envelope.sql", "entities.sql", "views.sql"]:
+ sql_file = schema_dir / f
+ if sql_file.is_file():
+ self.con.execute(sql_file.read_text(encoding="utf-8"))
+ else:
+ self._create_fallback_schema()
+
+ self.con.execute("""
+ CREATE OR REPLACE MACRO distance_m(lat1, lon1, lat2, lon2) AS (
+ 6371000.0 * 2.0 * asin(sqrt(
+ sin(radians(lat2 - lat1) / 2.0) * sin(radians(lat2 - lat1) / 2.0) +
+ cos(radians(lat1)) * cos(radians(lat2)) *
+ sin(radians(lon2 - lon1) / 2.0) * sin(radians(lon2 - lon1) / 2.0)
+ ))
+ );
+ """)
+
+ def _create_fallback_schema(self):
+ self.con.execute("""
+ CREATE TABLE IF NOT EXISTS sleep (
+ id VARCHAR PRIMARY KEY,
+ subject VARCHAR NOT NULL,
+ calendarDate DATE NOT NULL,
+ sleepType VARCHAR NOT NULL,
+ location VARCHAR,
+ isOpen BOOLEAN DEFAULT false,
+ startIso TIMESTAMPTZ NOT NULL,
+ endIso TIMESTAMPTZ,
+ sleepStartTimestampGMT BIGINT,
+ sleepEndTimestampGMT BIGINT,
+ sleepStartTimestampLocal BIGINT,
+ sleepEndTimestampLocal BIGINT,
+ sleepTimeSeconds INTEGER,
+ napTimeSeconds INTEGER,
+ deepSleepSeconds INTEGER,
+ lightSleepSeconds INTEGER,
+ remSleepSeconds INTEGER,
+ awakeSleepSeconds INTEGER,
+ awakeCount INTEGER,
+ avgSleepStress DOUBLE,
+ avgHeartRate DOUBLE,
+ averageSpO2Value DOUBLE,
+ lowestSpO2Value INTEGER,
+ highestSpO2Value INTEGER,
+ averageRespirationValue DOUBLE,
+ sleepScores JSON,
+ avgOvernightHrv DOUBLE,
+ hrvStatus VARCHAR,
+ bodyBatteryChange INTEGER,
+ restingHeartRate INTEGER,
+ restlessMomentsCount INTEGER,
+ note VARCHAR,
+ actor VARCHAR NOT NULL,
+ created_at TIMESTAMPTZ NOT NULL,
+ updated_at TIMESTAMPTZ,
+ deleted_at TIMESTAMPTZ
+ );
+
+ CREATE TABLE IF NOT EXISTS nutrition (
+ id VARCHAR PRIMARY KEY,
+ kind VARCHAR NOT NULL,
+ subject VARCHAR NOT NULL,
+ actor VARCHAR NOT NULL,
+ start TIMESTAMPTZ NOT NULL,
+ "end" TIMESTAMPTZ,
+ estimated BOOLEAN DEFAULT false,
+ meal VARCHAR,
+ note VARCHAR,
+ payload JSON NOT NULL,
+ source_ref VARCHAR,
+ created_at TIMESTAMPTZ NOT NULL,
+ updated_at TIMESTAMPTZ,
+ deleted_at TIMESTAMPTZ
+ );
+
+ CREATE TABLE IF NOT EXISTS mila (
+ id VARCHAR PRIMARY KEY,
+ kind VARCHAR NOT NULL,
+ subject VARCHAR NOT NULL DEFAULT 'mila',
+ actor VARCHAR NOT NULL,
+ start TIMESTAMPTZ NOT NULL,
+ "end" TIMESTAMPTZ,
+ note VARCHAR,
+ payload JSON NOT NULL,
+ source_ref VARCHAR,
+ created_at TIMESTAMPTZ NOT NULL,
+ updated_at TIMESTAMPTZ,
+ deleted_at TIMESTAMPTZ
+ );
+
+ CREATE TABLE IF NOT EXISTS people (
+ id VARCHAR PRIMARY KEY,
+ name VARCHAR NOT NULL,
+ role VARCHAR,
+ birthdate DATE,
+ notes VARCHAR
+ );
+
+ CREATE TABLE IF NOT EXISTS foods (
+ id VARCHAR PRIMARY KEY,
+ name VARCHAR NOT NULL,
+ brand VARCHAR,
+ category VARCHAR,
+ per_100 JSON,
+ density_g_per_ml DOUBLE,
+ ingredients_text VARCHAR,
+ barcode VARCHAR,
+ source VARCHAR,
+ description VARCHAR
+ );
+
+ CREATE TABLE IF NOT EXISTS recipes (
+ id VARCHAR PRIMARY KEY,
+ title VARCHAR NOT NULL,
+ servings INTEGER DEFAULT 1,
+ ingredients JSON,
+ estimated_delta JSON,
+ devices VARCHAR[],
+ tags VARCHAR[],
+ documents VARCHAR[],
+ description VARCHAR
+ );
+
+ CREATE TABLE IF NOT EXISTS places (
+ id VARCHAR PRIMARY KEY,
+ name VARCHAR NOT NULL,
+ category VARCHAR NOT NULL,
+ lat DOUBLE NOT NULL,
+ lon DOUBLE NOT NULL,
+ address VARCHAR,
+ google_maps_url VARCHAR,
+ tags VARCHAR[],
+ attributes JSON,
+ documents VARCHAR[],
+ notes VARCHAR
+ );
+
+ CREATE TABLE IF NOT EXISTS garmin_daily (
+ calendarDate DATE PRIMARY KEY,
+ totalKilocalories INTEGER,
+ activeKilocalories INTEGER,
+ bmrKilocalories INTEGER,
+ totalSteps INTEGER,
+ restingHeartRate INTEGER,
+ raw JSON
+ );
+ """)
+
+ def reload_all(self):
+ """Reloads all JSONL files from data_dir into DuckDB using fast vector-optimized read_json_auto."""
+ with self._lock:
+ # Clear tables
+ for tbl in ["sleep", "nutrition", "mila", "foods", "recipes", "places", "people", "garmin_daily"]:
+ try:
+ self.con.execute(f'DELETE FROM "{tbl}";')
+ except Exception:
+ pass
+
+ if not self.data_dir.is_dir():
+ return
+
+ # 1. Entities
+ entities_dir = self.data_dir / "entities"
+ if entities_dir.is_dir():
+ for coll in ["foods", "recipes", "places", "people"]:
+ f = entities_dir / f"{coll}.jsonl"
+ if f.is_file() and f.stat().st_size > 0:
+ try:
+ self.con.execute(f'INSERT OR REPLACE INTO "{coll}" SELECT * FROM read_json_auto(?, union_by_name=true)', [str(f)])
+ except Exception as e:
+ logger.warning(f"Fast load failed for {coll}, falling back: {e}")
+ self._load_jsonl_file(f, coll)
+
+ # 2. Nutrition
+ nutrition_files = list((self.data_dir / "nutrition").glob("*.jsonl")) if (self.data_dir / "nutrition").is_dir() else []
+ valid_nutrition = [str(f) for f in nutrition_files if f.stat().st_size > 0]
+ if valid_nutrition:
+ try:
+ self.con.execute('INSERT OR REPLACE INTO nutrition SELECT * FROM read_json_auto(?, union_by_name=true)', [valid_nutrition])
+ except Exception as e:
+ logger.warning(f"Fast load failed for nutrition, falling back: {e}")
+ for f in nutrition_files:
+ self._load_jsonl_file(f, "nutrition")
+
+ # 3. Sleep
+ sleep_files = list((self.data_dir / "sleep").glob("*.jsonl")) if (self.data_dir / "sleep").is_dir() else []
+ valid_sleep = [str(f) for f in sleep_files if f.stat().st_size > 0]
+ if valid_sleep:
+ try:
+ self.con.execute('INSERT OR REPLACE INTO sleep SELECT * FROM read_json_auto(?, union_by_name=true)', [valid_sleep])
+ except Exception as e:
+ logger.warning(f"Fast load failed for sleep, falling back: {e}")
+ for f in sleep_files:
+ self._load_jsonl_file(f, "sleep")
+
+ # 4. Mila
+ mila_files = list((self.data_dir / "mila").glob("*.jsonl")) if (self.data_dir / "mila").is_dir() else []
+ valid_mila = [str(f) for f in mila_files if f.stat().st_size > 0]
+ if valid_mila:
+ try:
+ self.con.execute(
+ """
+ INSERT OR REPLACE INTO mila (id, kind, subject, actor, start, "end", note, payload, source_ref, created_at, updated_at, deleted_at)
+ SELECT id, kind, subject, actor, start, "end", note, payload, source_ref, created_at, updated_at, deleted_at
+ FROM read_json_auto(?, union_by_name=true)
+ """,
+ [valid_mila],
+ )
+ except Exception as e:
+ logger.warning(f"Fast load failed for mila, falling back: {e}")
+ for f in mila_files:
+ self._load_jsonl_file(f, "mila")
+
+ # 5. Garmin daily
+ garmin_dir = self.data_dir / "garmin" / "daily"
+ garmin_files = list(garmin_dir.glob("*.jsonl")) if garmin_dir.is_dir() else []
+ valid_garmin = [str(f) for f in garmin_files if f.stat().st_size > 0]
+ if valid_garmin:
+ try:
+ self.con.execute(
+ """
+ INSERT OR REPLACE INTO garmin_daily
+ SELECT calendarDate, totalKilocalories, activeKilocalories, bmrKilocalories, totalSteps, restingHeartRate, to_json(r) AS raw
+ FROM read_json_auto(?, union_by_name=true) r
+ """,
+ [valid_garmin],
+ )
+ except Exception as e:
+ logger.warning(f"Fast load failed for garmin daily, falling back: {e}")
+ for f in garmin_files:
+ self._load_garmin_daily(f)
+
+ def _load_jsonl_file(self, file_path: Path, table_name: str):
+ if not file_path.is_file() or file_path.stat().st_size == 0:
+ return
+
+ allowed_cols = VALID_COLS_MAP.get(table_name)
+ with open(file_path, "r", encoding="utf-8") as f:
+ for line in f:
+ line = line.strip()
+ if not line:
+ continue
+ try:
+ r = json.loads(line)
+ if allowed_cols:
+ cols = [c for c in r.keys() if c in allowed_cols]
+ else:
+ cols = list(r.keys())
+ col_names = ", ".join([f'"{c}"' for c in cols])
+ placeholders = ", ".join(["?"] * len(cols))
+ vals = [json.dumps(r[c]) if isinstance(r[c], (dict, list)) else r[c] for c in cols]
+ self.con.execute(
+ f'INSERT OR REPLACE INTO "{table_name}" ({col_names}) VALUES ({placeholders})',
+ vals,
+ )
+ except Exception as e:
+ logger.warning(f"Error loading {table_name} record from {file_path}: {e}")
+
+ def _load_garmin_daily(self, file_path: Path):
+ if not file_path.is_file() or file_path.stat().st_size == 0:
+ return
+ with open(file_path, "r", encoding="utf-8") as f:
+ for line in f:
+ line = line.strip()
+ if not line:
+ continue
+ try:
+ r = json.loads(line)
+ cal_date = r.get("calendarDate")
+ if not cal_date:
+ continue
+ self.con.execute(
+ """
+ INSERT OR REPLACE INTO garmin_daily
+ (calendarDate, totalKilocalories, activeKilocalories, bmrKilocalories, totalSteps, restingHeartRate, raw)
+ VALUES (?, ?, ?, ?, ?, ?, ?)
+ """,
+ [
+ cal_date,
+ r.get("totalKilocalories"),
+ r.get("activeKilocalories"),
+ r.get("bmrKilocalories"),
+ r.get("totalSteps"),
+ r.get("restingHeartRate"),
+ json.dumps(r, ensure_ascii=False),
+ ],
+ )
+ except Exception as e:
+ logger.warning(f"Error loading garmin record from {file_path}: {e}")
+
+ def get_table_counts(self) -> Dict[str, int]:
+ with self._lock:
+ counts = {}
+ for tbl in ["sleep", "nutrition", "mila", "foods", "recipes", "places", "people", "garmin_daily"]:
+ try:
+ res = self.con.execute(f'SELECT count(*) FROM "{tbl}"').fetchone()
+ counts[tbl] = res[0] if res else 0
+ except Exception:
+ counts[tbl] = 0
+ return counts
+
+ # --------------------------------------------------------------------------
+ # Read Queries
+ # --------------------------------------------------------------------------
+ def query(self, sql: str, params: Optional[list] = None) -> List[Dict[str, Any]]:
+ clean_sql = sql.strip()
+ disallowed = ["DROP ", "ALTER ", "TRUNCATE ", "DELETE ", "UPDATE ", "INSERT "]
+ for d in disallowed:
+ if clean_sql.upper().startswith(d):
+ raise ValueError(f"Direct {d.strip()} queries not permitted via query(). Use dedicated write methods.")
+
+ with self._lock:
+ cursor = self.con.execute(clean_sql, params or [])
+ cols = [desc[0] for desc in cursor.description]
+ rows = cursor.fetchall()
+ results = []
+ for row in rows:
+ rec = {}
+ for col, val in zip(cols, row):
+ if isinstance(val, (datetime.datetime, datetime.date)):
+ rec[col] = val.isoformat()
+ else:
+ rec[col] = val
+ results.append(rec)
+ return results
+
+ # --------------------------------------------------------------------------
+ # Curated Mila Dashboard Payload
+ # --------------------------------------------------------------------------
+ def get_mila_dashboard(self, as_of_date: Optional[str] = None) -> Dict[str, Any]:
+ t0 = time.perf_counter()
+ now_berlin = datetime.datetime.now(BERLIN_TZ)
+ today_date = datetime.date.fromisoformat(as_of_date) if as_of_date else now_berlin.date()
+ today_str = today_date.strftime("%Y-%m-%d")
+ today_pretty = f"{WEEKDAYS_LONG_DE[today_date.weekday()]}, {today_date.strftime('%d.%m.%Y')}"
+ age_str = calculate_age_str(today_date)
+
+ with self._lock:
+ # 1. Active open sleep check
+ open_sleep = self.con.execute(
+ """
+ SELECT id, start, local_start, sleep_type, location, note,
+ date_diff('minute', start, CURRENT_TIMESTAMP) AS duration_mins
+ FROM v_sleep
+ WHERE subject = 'mila' AND is_open = true
+ ORDER BY start DESC LIMIT 1
+ """
+ ).fetchone()
+
+ is_sleeping = open_sleep is not None
+ active_sleep_id = open_sleep[0] if is_sleeping else None
+ active_sleep_duration_str = ""
+ active_sleep_start_time = ""
+ active_sleep_iso = ""
+ wake_window_str = ""
+ wake_window_minutes = 0
+ last_wake_time_str = ""
+ last_wake_iso = ""
+
+ if is_sleeping:
+ dur = max(0, open_sleep[6] or 0)
+ h, m = divmod(dur, 60)
+ active_sleep_duration_str = f"{h} h {m:02d} min" if h > 0 else f"{m} min"
+ s_dt = datetime.datetime.fromisoformat(str(open_sleep[2]))
+ active_sleep_start_time = s_dt.strftime("%H:%M")
+ active_sleep_iso = s_dt.isoformat()
+ wake_window_minutes = 0
+ else:
+ last_closed = self.con.execute(
+ """
+ SELECT "end", local_end, date_diff('minute', "end", CURRENT_TIMESTAMP) AS mins_ago
+ FROM v_sleep
+ WHERE subject = 'mila' AND is_open = false AND "end" IS NOT NULL
+ ORDER BY "end" DESC LIMIT 1
+ """
+ ).fetchone()
+ if last_closed:
+ mins_ago = max(0, last_closed[2] or 0)
+ wake_window_minutes = mins_ago
+ h, m = divmod(mins_ago, 60)
+ wake_window_str = f"{h} h {m:02d} min" if h > 0 else f"{m} min"
+ e_dt = datetime.datetime.fromisoformat(str(last_closed[1]))
+ last_wake_time_str = e_dt.strftime("%H:%M")
+ last_wake_iso = e_dt.isoformat()
+
+ # Parent Cockpit Live Status: Last Feed & Last Diaper
+ last_feed = self.con.execute(
+ """
+ SELECT "start", local_time, date_diff('minute', "start", CURRENT_TIMESTAMP) AS mins_ago,
+ consumed_ml, solid_g, item, kind
+ FROM v_feeding
+ WHERE subject = 'mila' AND "start" IS NOT NULL
+ ORDER BY "start" DESC LIMIT 1
+ """
+ ).fetchone()
+
+ last_feed_iso = None
+ last_feed_time_str = None
+ last_feed_since_str = "—"
+ last_feed_since_minutes = None
+ last_feed_summary = None
+
+ if last_feed:
+ f_mins = max(0, last_feed[2] or 0)
+ last_feed_since_minutes = f_mins
+ f_h, f_m = divmod(f_mins, 60)
+ last_feed_since_str = f"{f_h} h {f_m:02d} min" if f_h > 0 else f"{f_m} min"
+ f_dt = datetime.datetime.fromisoformat(str(last_feed[1]))
+ last_feed_time_str = f_dt.strftime("%H:%M")
+ last_feed_iso = f_dt.isoformat()
+ if last_feed[3]:
+ last_feed_summary = f"{last_feed[3]} ml"
+ elif last_feed[4]:
+ last_feed_summary = f"{last_feed[4]} g {last_feed[5] or ''}".strip()
+ else:
+ last_feed_summary = last_feed[5] or "Mahlzeit"
+
+ last_diaper = self.con.execute(
+ """
+ SELECT "start", local_time, date_diff('minute', "start", CURRENT_TIMESTAMP) AS mins_ago,
+ pee, poo, texture
+ FROM v_diaper
+ WHERE subject = 'mila' AND "start" IS NOT NULL
+ ORDER BY "start" DESC LIMIT 1
+ """
+ ).fetchone()
+
+ last_diaper_iso = None
+ last_diaper_time_str = None
+ last_diaper_since_str = "—"
+ last_diaper_since_minutes = None
+ last_diaper_type = None
+
+ if last_diaper:
+ d_mins = max(0, last_diaper[2] or 0)
+ last_diaper_since_minutes = d_mins
+ d_h, d_m = divmod(d_mins, 60)
+ last_diaper_since_str = f"{d_h} h {d_m:02d} min" if d_h > 0 else f"{d_m} min"
+ d_dt = datetime.datetime.fromisoformat(str(last_diaper[1]))
+ last_diaper_time_str = d_dt.strftime("%H:%M")
+ last_diaper_iso = d_dt.isoformat()
+ if last_diaper[3] and last_diaper[4]:
+ last_diaper_type = "Pipi & Stuhl"
+ elif last_diaper[4]:
+ last_diaper_type = "Stuhl"
+ elif last_diaper[3]:
+ last_diaper_type = "Pipi"
+ else:
+ last_diaper_type = "Windel"
+
+ last_stool = self.con.execute(
+ """
+ SELECT "start", local_time, date_diff('minute', "start", CURRENT_TIMESTAMP) AS mins_ago
+ FROM v_diaper
+ WHERE subject = 'mila' AND poo = true AND "start" IS NOT NULL
+ ORDER BY "start" DESC LIMIT 1
+ """
+ ).fetchone()
+
+ last_stool_iso = None
+ last_stool_str = "—"
+ if last_stool:
+ st_mins = max(0, last_stool[2] or 0)
+ st_h, st_m = divmod(st_mins, 60)
+ last_stool_str = f"{st_h} h {st_m:02d} min" if st_h > 0 else f"{st_m} min"
+ st_dt = datetime.datetime.fromisoformat(str(last_stool[1]))
+ last_stool_iso = st_dt.isoformat()
+
+ # 2. Today's sleep sessions
+ today_sessions = self.con.execute(
+ f"""
+ SELECT id, strftime(local_start, '%H:%M') AS start_fmt,
+ CASE WHEN is_open THEN NULL ELSE strftime(local_end, '%H:%M') END AS end_fmt,
+ sleep_type, location, note, duration_minutes, is_open
+ FROM v_sleep
+ WHERE subject = 'mila' AND (
+ log_date = '{today_str}'
+ OR (sleep_type = 'night' AND CAST(local_end AS DATE) = '{today_str}')
+ )
+ ORDER BY start ASC
+ """
+ ).fetchall()
+
+ today_naps = []
+ today_nights = []
+ for s in today_sessions:
+ s_dict = {
+ "id": s[0],
+ "start": s[1],
+ "end": s[2],
+ "sleep_type": s[3],
+ "location": s[4] or "",
+ "note": s[5] or "",
+ "duration_minutes": s[6],
+ "is_open": bool(s[7]),
+ }
+ if s[3] == "night":
+ today_nights.append(s_dict)
+ else:
+ today_naps.append(s_dict)
+
+ # Daily sleep aggregate
+ today_agg = self.con.execute(
+ f"""
+ SELECT total_sleep_hours, nap_count, night_sleep_minutes, nap_sleep_minutes
+ FROM v_sleep_daily
+ WHERE subject = 'mila' AND log_date = '{today_str}'
+ """
+ ).fetchone()
+
+ morning_night = next((s for s in today_nights if not s["is_open"] and s["duration_minutes"]), None)
+ if morning_night and morning_night["duration_minutes"]:
+ today_night_min = morning_night["duration_minutes"]
+ else:
+ today_night_min = today_agg[2] if today_agg and today_agg[2] is not None else 0
+
+ today_nap_count = today_agg[1] if today_agg and today_agg[1] is not None else len(today_naps)
+ today_nap_min = sum(n["duration_minutes"] or 0 for n in today_naps)
+ today_tot_min = today_night_min + today_nap_min
+ today_tot_hours = f"{today_tot_min / 60.0:.2f} h" if today_tot_min > 0 else "0 h"
+
+ today_night_h, today_night_m = divmod(today_night_min, 60)
+ today_night_dur_str = f"{today_night_h} h {today_night_m:02d} min" if today_night_h > 0 else f"{today_night_m} min"
+
+ # 3. Today's feedings
+ feedings_rows = self.con.execute(
+ f"""
+ SELECT id, strftime(local_time, '%H:%M') AS time_str,
+ consumed_ml, milk_type, item, solid_g, kcal, kind, note
+ FROM v_feeding
+ WHERE subject = 'mila' AND log_date = '{today_str}'
+ ORDER BY start ASC
+ """
+ ).fetchall()
+
+ feedings_list = []
+ for f in feedings_rows:
+ feedings_list.append({
+ "id": f[0],
+ "time": f[1],
+ "consumed_ml": f[2],
+ "milk_type": f[3] or "",
+ "item": f[4] or "",
+ "solid_g": f[5],
+ "kcal": f[6],
+ "kind": f[7],
+ "note": f[8] or "",
+ })
+
+ feed_agg = self.con.execute(
+ f"""
+ SELECT bottle_count, total_consumed_ml, solid_count, total_solid_g, total_kcal
+ FROM v_feeding_daily
+ WHERE subject = 'mila' AND log_date = '{today_str}'
+ """
+ ).fetchone()
+
+ today_bottle_count = int(feed_agg[0]) if feed_agg and feed_agg[0] is not None else 0
+ today_bottle_ml = int(feed_agg[1]) if feed_agg and feed_agg[1] is not None else 0
+ today_solid_count = int(feed_agg[2]) if feed_agg and feed_agg[2] is not None else 0
+ today_solid_g = int(feed_agg[3]) if feed_agg and feed_agg[3] is not None else 0
+ today_feeding_kcal = int(feed_agg[4]) if feed_agg and feed_agg[4] is not None else 0
+
+ # 4. Today's diapers
+ diaper_rows = self.con.execute(
+ f"""
+ SELECT id, strftime(local_time, '%H:%M') AS time_str, pee, poo, texture, note
+ FROM v_diaper
+ WHERE subject = 'mila' AND log_date = '{today_str}'
+ ORDER BY start ASC
+ """
+ ).fetchall()
+
+ diaper_list = []
+ for d in diaper_rows:
+ diaper_list.append({
+ "id": d[0],
+ "time": d[1],
+ "pee": bool(d[2]),
+ "poo": bool(d[3]),
+ "texture": d[4] or "",
+ "note": d[5] or "",
+ })
+
+ diaper_agg = self.con.execute(
+ f"""
+ SELECT total_changes, wet_count, dirty_count
+ FROM v_diaper_daily
+ WHERE subject = 'mila' AND log_date = '{today_str}'
+ """
+ ).fetchone()
+
+ today_diaper_count = int(diaper_agg[0]) if diaper_agg and diaper_agg[0] is not None else 0
+ today_wet_count = int(diaper_agg[1]) if diaper_agg and diaper_agg[1] is not None else 0
+ today_dirty_count = int(diaper_agg[2]) if diaper_agg and diaper_agg[2] is not None else 0
+
+ # 5. 7d and 30d sleep averages
+ avg_7 = self.con.execute(
+ """
+ SELECT ROUND(AVG(total_sleep_hours), 2), ROUND(AVG(night_sleep_minutes)/60.0, 2),
+ ROUND(AVG(nap_sleep_minutes)/60.0, 2), ROUND(AVG(nap_count), 1)
+ FROM (SELECT total_sleep_hours, night_sleep_minutes, nap_sleep_minutes, nap_count
+ FROM v_sleep_daily WHERE subject = 'mila' ORDER BY log_date DESC LIMIT 7)
+ """
+ ).fetchone()
+
+ avg_7_tot = f"{avg_7[0]:.2f} h" if avg_7 and avg_7[0] is not None else "—"
+ avg_7_night = f"{avg_7[1]:.2f} h" if avg_7 and avg_7[1] is not None else "—"
+ avg_7_naps = f"{avg_7[3]} Naps ({avg_7[2]:.2f} h)" if avg_7 and avg_7[3] is not None else "—"
+
+ avg_30 = self.con.execute(
+ """
+ SELECT ROUND(AVG(total_sleep_hours), 2)
+ FROM (SELECT total_sleep_hours FROM v_sleep_daily WHERE subject = 'mila' ORDER BY log_date DESC LIMIT 30)
+ """
+ ).fetchone()
+ avg_30_tot = f"{avg_30[0]:.2f} h" if avg_30 and avg_30[0] is not None else "—"
+
+ feed_avg_7 = self.con.execute(
+ """
+ SELECT ROUND(AVG(total_consumed_ml), 0), ROUND(AVG(total_solid_g), 0),
+ ROUND(AVG(total_kcal), 0), ROUND(AVG(bottle_count), 1)
+ FROM (SELECT total_consumed_ml, total_solid_g, total_kcal, bottle_count
+ FROM v_feeding_daily WHERE subject = 'mila' ORDER BY log_date DESC LIMIT 7)
+ """
+ ).fetchone()
+
+ diaper_avg_7 = self.con.execute(
+ """
+ SELECT ROUND(AVG(wet_count), 1), ROUND(AVG(total_changes), 1)
+ FROM (SELECT wet_count, total_changes
+ FROM v_diaper_daily WHERE subject = 'mila' ORDER BY log_date DESC LIMIT 7)
+ """
+ ).fetchone()
+
+ # 6. Past 30 Days History Table & sessions
+ history_rows_data = self.con.execute(
+ """
+ WITH dates AS (
+ SELECT DISTINCT log_date FROM v_sleep_daily WHERE subject = 'mila'
+ UNION
+ SELECT DISTINCT log_date FROM v_feeding_daily WHERE subject = 'mila'
+ UNION
+ SELECT DISTINCT log_date FROM v_diaper_daily WHERE subject = 'mila'
+ )
+ SELECT
+ dt.log_date,
+ COALESCE(s.total_sleep_hours, 0.0) AS total_sleep_hours,
+ COALESCE(s.night_sleep_minutes, 0) AS night_sleep_minutes,
+ COALESCE(s.nap_count, 0) AS nap_count,
+ COALESCE(s.nap_sleep_minutes, 0) AS nap_sleep_minutes,
+ COALESCE(f.total_consumed_ml, 0) AS feed_ml,
+ COALESCE(f.bottle_count, 0) AS feed_bottles,
+ COALESCE(f.total_solid_g, 0) AS feed_solid_g,
+ COALESCE(f.solid_count, 0) AS feed_solid_count,
+ COALESCE(d.total_changes, 0) AS diaper_cnt,
+ COALESCE(d.dirty_count, 0) AS diaper_dirty,
+ COALESCE(d.wet_count, 0) AS diaper_wet,
+ COALESCE(f.total_kcal, 0) AS feed_kcal
+ FROM dates dt
+ LEFT JOIN v_sleep_daily s ON dt.log_date = s.log_date AND s.subject = 'mila'
+ LEFT JOIN v_feeding_daily f ON dt.log_date = f.log_date AND f.subject = 'mila'
+ LEFT JOIN v_diaper_daily d ON dt.log_date = d.log_date AND d.subject = 'mila'
+ ORDER BY dt.log_date DESC
+ LIMIT 30
+ """
+ ).fetchall()
+
+ all_recent_sessions = self.con.execute(
+ """
+ SELECT log_date, strftime(local_start, '%H:%M'),
+ CASE WHEN is_open THEN NULL ELSE strftime(local_end, '%H:%M') END,
+ sleep_type, duration_minutes, is_open, note, local_start, local_end
+ FROM v_sleep
+ WHERE subject = 'mila' AND log_date >= (CURRENT_DATE - INTERVAL 30 DAY)
+ ORDER BY start ASC
+ """
+ ).fetchall()
+
+ sessions_by_date: Dict[str, list] = {}
+ for row in all_recent_sessions:
+ d_key = str(row[0])
+ if d_key not in sessions_by_date:
+ sessions_by_date[d_key] = []
+ sessions_by_date[d_key].append({
+ "start": row[1],
+ "end": row[2] or "läuft",
+ "type": row[3],
+ "duration_minutes": row[4],
+ "is_open": bool(row[5]),
+ "note": row[6] or "",
+ "local_start": str(row[7]),
+ "local_end": str(row[8]) if row[8] else None,
+ })
+
+ history_list = []
+ for r in history_rows_data:
+ d_str = str(r[0])
+ d_obj = datetime.date.fromisoformat(d_str)
+ nh, nm = divmod(r[2] or 0, 60)
+ d_sessions = sessions_by_date.get(d_str, [])
+ night_durs = [s["duration_minutes"] for s in d_sessions if s["type"] == "night" and s["duration_minutes"]]
+ longest_night = max(night_durs) if night_durs else int(r[2] or 0)
+
+ wake_windows = []
+ day_sessions = [s for s in d_sessions if s["type"] == "nap" and s.get("local_start") and s.get("local_end")]
+ for i in range(len(day_sessions) - 1):
+ try:
+ e_prev = datetime.datetime.fromisoformat(day_sessions[i]["local_end"])
+ s_next = datetime.datetime.fromisoformat(day_sessions[i + 1]["local_start"])
+ diff = int((s_next - e_prev).total_seconds() / 60)
+ if 30 <= diff <= 360:
+ wake_windows.append(diff)
+ except Exception:
+ pass
+ avg_wake = round(sum(wake_windows) / len(wake_windows)) if wake_windows else None
+
+ history_list.append({
+ "date": d_str,
+ "weekday": WEEKDAYS_DE[d_obj.weekday()],
+ "formatted_date": d_obj.strftime("%d.%m."),
+ "is_today": (d_str == today_str),
+ "total_hours": round(r[1], 2) if r[1] is not None else 0.0,
+ "night_minutes": int(r[2] or 0),
+ "night_duration_str": f"{nh} h {nm:02d} min" if nh > 0 else f"{nm} min",
+ "nap_minutes": int(r[4] or 0),
+ "nap_count": int(r[3] or 0),
+ "nap_str": f"{r[3]} ({r[4]} min)",
+ "longest_night_stretch_min": longest_night,
+ "avg_wake_window_min": avg_wake,
+ "feed_bottles_ml": int(r[5]),
+ "feed_bottles_count": int(r[6]),
+ "feed_solid_g": int(r[7]),
+ "feed_solid_count": int(r[8]),
+ "feed_total_kcal": int(r[12] or 0),
+ "diaper_changes": int(r[9]),
+ "diaper_dirty": int(r[10]),
+ "diaper_wet": int(r[11] or 0),
+ "sessions": d_sessions,
+ })
+
+ # 7. Growth Measurements & Percentiles
+ growth_rows = self.con.execute(
+ """
+ SELECT id, log_date, weight_kg, height_cm, head_circ_cm, note
+ FROM v_growth
+ WHERE subject = 'mila'
+ ORDER BY start ASC
+ """
+ ).fetchall()
+
+ growth_measurements = []
+ for row in growth_rows:
+ r_id, r_date, r_weight, r_height, r_hc, r_note = row
+ d_obj = datetime.date.fromisoformat(str(r_date))
+ age_days = (d_obj - MILA_BIRTHDATE).days
+ age_months = max(0.0, age_days / 30.4375)
+
+ pw = calculate_percentile("weight", r_weight, age_months) if r_weight is not None else None
+ ph = calculate_percentile("height", r_height, age_months) if r_height is not None else None
+
+ growth_measurements.append({
+ "id": r_id,
+ "date_str": str(r_date),
+ "date_pretty": d_obj.strftime("%d.%m.%Y"),
+ "age_str": calculate_age_str(d_obj),
+ "age_months": round(age_months, 1),
+ "weight_kg": r_weight,
+ "weight_pct": pw["percentile_str"] if pw else "—",
+ "height_cm": r_height,
+ "height_pct": ph["percentile_str"] if ph else "—",
+ "head_circ_cm": r_hc,
+ "note": r_note or "",
+ })
+
+ latest_weight_kg = growth_measurements[-1]["weight_kg"] if growth_measurements and growth_measurements[-1]["weight_kg"] else 7.5
+ age_days_calc = (today_date - MILA_BIRTHDATE).days
+ age_months_calc = max(0.0, age_days_calc / 30.4375)
+
+ age_norms = {
+ "age_months": round(age_months_calc, 1),
+ "sleep_total_hours_min": 12.0,
+ "sleep_total_hours_max": 15.0,
+ "night_sleep_hours_min": 10.0,
+ "night_sleep_hours_max": 11.5,
+ "longest_night_stretch_hours_min": 5.0,
+ "longest_night_stretch_hours_max": 8.5,
+ "nap_sleep_hours_min": 2.5,
+ "nap_sleep_hours_max": 3.5,
+ "nap_count_min": 2,
+ "nap_count_max": 3,
+ "wake_window_min_minutes": 135,
+ "wake_window_max_minutes": 180,
+ "feed_interval_min_minutes": 180,
+ "feed_interval_max_minutes": 240,
+ "milk_ml_min": 500,
+ "milk_ml_max": 800,
+ "bottle_size_ml_min": 150,
+ "bottle_size_ml_max": 210,
+ "solid_g_min": 150,
+ "solid_g_max": 380,
+ "solid_meals_target": "1–2 Breimahlzeiten (Mittags- & Abendbrei)",
+ "energy_kcal_target": round(latest_weight_kg * 80) if latest_weight_kg else 600,
+ "energy_kcal_min": 550,
+ "energy_kcal_max": 700,
+ "wet_diapers_min": 4,
+ "wet_diapers_max": 6,
+ }
+
+ execution_time_ms = round((time.perf_counter() - t0) * 1000, 2)
+ git_status = self.git_sync_manager.status if self.git_sync_manager else "ok"
+ git_conflict = self.git_sync_manager.is_conflict if self.git_sync_manager else False
+
+ return {
+ "status": "ok",
+ "execution_time_ms": execution_time_ms,
+ "git_status": git_status,
+ "git_conflict": git_conflict,
+ "metadata": {
+ "subject": "mila",
+ "today_date": today_str,
+ "today_pretty": today_pretty,
+ "age_str": age_str,
+ },
+ "live_status": {
+ "is_sleeping": is_sleeping,
+ "active_sleep_id": active_sleep_id,
+ "active_sleep_duration_str": active_sleep_duration_str,
+ "active_sleep_start_time": active_sleep_start_time,
+ "active_sleep_iso": active_sleep_iso,
+ "wake_window_str": wake_window_str,
+ "wake_window_minutes": wake_window_minutes,
+ "last_wake_time_str": last_wake_time_str,
+ "last_wake_iso": last_wake_iso,
+ "last_feed_iso": last_feed_iso,
+ "last_feed_time_str": last_feed_time_str,
+ "last_feed_since_str": last_feed_since_str,
+ "last_feed_since_minutes": last_feed_since_minutes,
+ "last_feed_summary": last_feed_summary,
+ "last_diaper_iso": last_diaper_iso,
+ "last_diaper_time_str": last_diaper_time_str,
+ "last_diaper_since_str": last_diaper_since_str,
+ "last_diaper_since_minutes": last_diaper_since_minutes,
+ "last_diaper_type": last_diaper_type,
+ "last_stool_iso": last_stool_iso,
+ "last_stool_str": last_stool_str,
+ },
+ "today": {
+ "total_sleep_hours_str": today_tot_hours,
+ "nap_count": today_nap_count,
+ "nap_minutes": today_nap_min,
+ "night_duration_str": today_night_dur_str,
+ "naps": today_naps,
+ "nights": today_nights,
+ "feedings": feedings_list,
+ "feeding_summary": {
+ "bottle_count": today_bottle_count,
+ "total_consumed_ml": today_bottle_ml,
+ "solid_count": today_solid_count,
+ "total_solid_g": today_solid_g,
+ "total_kcal": today_feeding_kcal,
+ },
+ "diapers": diaper_list,
+ "diaper_summary": {
+ "total_changes": today_diaper_count,
+ "wet_count": today_wet_count,
+ "dirty_count": today_dirty_count,
+ },
+ },
+ "averages": {
+ "avg_7_tot": avg_7_tot,
+ "avg_7_night": avg_7_night,
+ "avg_7_naps": avg_7_naps,
+ "avg_30_tot": avg_30_tot,
+ "avg_7_feed_ml": int(feed_avg_7[0]) if feed_avg_7 and feed_avg_7[0] is not None else 0,
+ "avg_7_solid_g": int(feed_avg_7[1]) if feed_avg_7 and feed_avg_7[1] is not None else 0,
+ "avg_7_kcal": int(feed_avg_7[2]) if feed_avg_7 and feed_avg_7[2] is not None else 0,
+ "avg_7_bottles": float(feed_avg_7[3]) if feed_avg_7 and feed_avg_7[3] is not None else 0.0,
+ "avg_7_diapers_wet": float(diaper_avg_7[0]) if diaper_avg_7 and diaper_avg_7[0] is not None else 0.0,
+ "avg_7_diapers_total": float(diaper_avg_7[1]) if diaper_avg_7 and diaper_avg_7[1] is not None else 0.0,
+ },
+ "history": history_list,
+ "growth": {
+ "measurements": growth_measurements,
+ "latest": growth_measurements[-1] if growth_measurements else None,
+ },
+ "age_norms": age_norms,
+ }
+
+ # --------------------------------------------------------------------------
+ # Curated Moritz Dashboard Payload
+ # --------------------------------------------------------------------------
+ def get_moritz_dashboard(self, as_of_date: Optional[str] = None) -> Dict[str, Any]:
+ t0 = time.perf_counter()
+ now_berlin = datetime.datetime.now(BERLIN_TZ)
+ today_date = datetime.date.fromisoformat(as_of_date) if as_of_date else now_berlin.date()
+ yesterday_date = today_date - datetime.timedelta(days=1)
+ today_str = today_date.strftime("%Y-%m-%d")
+ yesterday_str = yesterday_date.strftime("%Y-%m-%d")
+
+ today_pretty = f"{WEEKDAYS_LONG_DE[today_date.weekday()]}, {today_date.strftime('%d.%m.%Y')}"
+ yesterday_pretty = f"{WEEKDAYS_LONG_DE[yesterday_date.weekday()]}, {yesterday_date.strftime('%d.%m.%Y')}"
+
+ with self._lock:
+ # 1. Heute Nutrition
+ today_summary = self.con.execute(
+ """
+ SELECT total_protein_g, total_carbs_g, total_sugar_g, total_fat_g, total_fiber_g, total_salt_g, total_kcal, item_count
+ FROM v_nutrition_daily
+ WHERE subject = 'moritz' AND log_date = ?
+ """,
+ [today_str],
+ ).fetchone()
+
+ today_kcal = round(today_summary[6]) if today_summary and today_summary[6] is not None else 0
+ today_protein = round(today_summary[0], 1) if today_summary and today_summary[0] is not None else 0.0
+ today_carbs = round(today_summary[1], 1) if today_summary and today_summary[1] is not None else 0.0
+ today_fat = round(today_summary[3], 1) if today_summary and today_summary[3] is not None else 0.0
+ today_count = int(today_summary[7]) if today_summary and today_summary[7] is not None else 0
+
+ today_meals_rows = self.con.execute(
+ """
+ SELECT id, strftime(local_time, '%H:%M') AS time_str, item, qty, unit, protein_g, carbs_g, fat_g, fiber_g, kcal, estimated
+ FROM v_nutrition
+ WHERE subject = 'moritz' AND log_date = ?
+ ORDER BY start ASC
+ """,
+ [today_str],
+ ).fetchall()
+
+ today_meals = []
+ for m in today_meals_rows:
+ today_meals.append({
+ "id": m[0],
+ "time": m[1],
+ "item": m[2] or "",
+ "qty": m[3],
+ "unit": m[4] or "g",
+ "protein_g": round(m[5], 1) if m[5] is not None else 0.0,
+ "carbs_g": round(m[6], 1) if m[6] is not None else 0.0,
+ "fat_g": round(m[7], 1) if m[7] is not None else 0.0,
+ "fiber_g": round(m[8], 1) if m[8] is not None else 0.0,
+ "kcal": round(m[9]) if m[9] is not None else 0,
+ "estimated": bool(m[10]),
+ })
+
+ # 2. Gestern 24h Netto-Bilanz
+ yesterday_balance = self.con.execute(
+ """
+ SELECT intake_kcal, expenditure_kcal, active_kcal, rmr_kcal, steps, balance_kcal, balance_status, protein_g, carbs_g, fat_g
+ FROM v_daily_balance
+ WHERE subject = 'moritz' AND log_date = ?
+ """,
+ [yesterday_str],
+ ).fetchone()
+
+ if not yesterday_balance or yesterday_balance[1] is None:
+ g_row = self.con.execute(
+ "SELECT total_expenditure_kcal, active_kcal, rmr_kcal, steps, resting_hr FROM v_garmin_daily WHERE log_date = ?",
+ [yesterday_str],
+ ).fetchone()
+ n_row = self.con.execute(
+ "SELECT total_kcal, total_protein_g, total_carbs_g, total_fat_g FROM v_nutrition_daily WHERE subject = 'moritz' AND log_date = ?",
+ [yesterday_str],
+ ).fetchone()
+
+ intake_y = round(n_row[0]) if n_row and n_row[0] is not None else 0
+ exp_y = round(g_row[0]) if g_row and g_row[0] is not None else 0
+ act_y = round(g_row[1]) if g_row and g_row[1] is not None else 0
+ rmr_y = round(g_row[2]) if g_row and g_row[2] is not None else 0
+ steps_y = int(g_row[3]) if g_row and g_row[3] is not None else 0
+ if exp_y > 0:
+ bal_y = intake_y - exp_y
+ status_y = "Defizit" if bal_y < 0 else ("Überschuss" if bal_y > 0 else "Ausgeglichen")
+ else:
+ bal_y = None
+ status_y = "Garmin ausstehend"
+ prot_y = round(n_row[1], 1) if n_row and n_row[1] is not None else 0.0
+ carbs_y = round(n_row[2], 1) if n_row and n_row[2] is not None else 0.0
+ fat_y = round(n_row[3], 1) if n_row and n_row[3] is not None else 0.0
+ rhr_y = int(g_row[4]) if g_row and g_row[4] is not None else None
+ else:
+ intake_y = round(yesterday_balance[0]) if yesterday_balance[0] is not None else 0
+ exp_y = round(yesterday_balance[1]) if yesterday_balance[1] is not None else 0
+ act_y = round(yesterday_balance[2]) if yesterday_balance[2] is not None else 0
+ rmr_y = round(yesterday_balance[3]) if yesterday_balance[3] is not None else 0
+ steps_y = int(yesterday_balance[4]) if yesterday_balance[4] is not None else 0
+ bal_y = round(yesterday_balance[5]) if yesterday_balance[5] is not None else (intake_y - exp_y if exp_y > 0 else None)
+ status_y = yesterday_balance[6] or ("Garmin ausstehend" if not exp_y else "Ausgeglichen")
+ prot_y = round(yesterday_balance[7], 1) if yesterday_balance[7] is not None else 0.0
+ carbs_y = round(yesterday_balance[8], 1) if yesterday_balance[8] is not None else 0.0
+ fat_y = round(yesterday_balance[9], 1) if yesterday_balance[9] is not None else 0.0
+
+ rhr_check = self.con.execute(
+ "SELECT resting_hr FROM v_garmin_daily WHERE log_date = ?", [yesterday_str]
+ ).fetchone()
+ rhr_y = int(rhr_check[0]) if rhr_check and rhr_check[0] is not None else None
+
+ yesterday_meals_rows = self.con.execute(
+ """
+ SELECT strftime(local_time, '%H:%M') AS time_str, item, qty, unit, protein_g, carbs_g, fat_g, fiber_g, kcal
+ FROM v_nutrition
+ WHERE subject = 'moritz' AND log_date = ?
+ ORDER BY start ASC
+ """,
+ [yesterday_str],
+ ).fetchall()
+
+ yesterday_meals = []
+ for m in yesterday_meals_rows:
+ yesterday_meals.append({
+ "time": m[0],
+ "item": m[1] or "",
+ "qty": m[2],
+ "unit": m[3] or "g",
+ "protein_g": round(m[4], 1) if m[4] is not None else 0.0,
+ "carbs_g": round(m[5], 1) if m[5] is not None else 0.0,
+ "fat_g": round(m[6], 1) if m[6] is not None else 0.0,
+ "fiber_g": round(m[7], 1) if m[7] is not None else 0.0,
+ "kcal": round(m[8]) if m[8] is not None else 0,
+ })
+
+ # 3. Garmin Sleep & Recovery
+ sleep_y_row = self.con.execute(
+ """
+ SELECT total_sleep_hours, total_sleep_minutes, sleep_score, sleep_score_qualifier,
+ avg_overnight_hrv, hrv_status, body_battery_change, resting_heart_rate,
+ deep_sleep_minutes, light_sleep_minutes, rem_sleep_minutes, awake_sleep_minutes
+ FROM v_sleep_daily
+ WHERE subject = 'moritz' AND log_date = ?
+ """,
+ [yesterday_str],
+ ).fetchone()
+
+ sleep_date_label = yesterday_pretty
+ if not sleep_y_row or sleep_y_row[1] is None or sleep_y_row[1] == 0:
+ latest_sleep = self.con.execute(
+ """
+ SELECT total_sleep_hours, total_sleep_minutes, sleep_score, sleep_score_qualifier,
+ avg_overnight_hrv, hrv_status, body_battery_change, resting_heart_rate,
+ deep_sleep_minutes, light_sleep_minutes, rem_sleep_minutes, awake_sleep_minutes, log_date
+ FROM v_sleep_daily
+ WHERE subject = 'moritz' AND total_sleep_minutes > 0
+ ORDER BY log_date DESC LIMIT 1
+ """
+ ).fetchone()
+ if latest_sleep:
+ sleep_y_row = latest_sleep[:12]
+ d_obj = datetime.date.fromisoformat(str(latest_sleep[12]))
+ sleep_date_label = f"{WEEKDAYS_LONG_DE[d_obj.weekday()]}, {d_obj.strftime('%d.%m.%Y')}"
+
+ if sleep_y_row and sleep_y_row[1]:
+ sleep_dur_tot_m = int(sleep_y_row[1])
+ s_dh, s_dm = divmod(sleep_dur_tot_m, 60)
+ sleep_dur_str = f"{s_dh}h {s_dm:02d}m"
+ sleep_score = sleep_y_row[2]
+ sleep_qualifier = sleep_y_row[3] or ""
+ hrv_val = round(sleep_y_row[4]) if sleep_y_row[4] is not None else None
+ hrv_status = sleep_y_row[5] or ""
+ bb_change = sleep_y_row[6]
+ rhr_val = sleep_y_row[7] or rhr_y
+ deep_m = int(sleep_y_row[8] or 0)
+ light_m = int(sleep_y_row[9] or 0)
+ rem_m = int(sleep_y_row[10] or 0)
+ awake_m = int(sleep_y_row[11] or 0)
+ tot_stages = (deep_m + light_m + rem_m + awake_m) or 1
+ deep_pct = round((deep_m / tot_stages) * 100, 1)
+ light_pct = round((light_m / tot_stages) * 100, 1)
+ rem_pct = round((rem_m / tot_stages) * 100, 1)
+ awake_pct = round((awake_m / tot_stages) * 100, 1)
+ else:
+ sleep_dur_str = "—"
+ sleep_score = None
+ sleep_qualifier = ""
+ hrv_val = None
+ hrv_status = ""
+ bb_change = None
+ rhr_val = rhr_y
+ deep_m = light_m = rem_m = awake_m = 0
+ deep_pct = light_pct = rem_pct = awake_pct = 0.0
+
+ # 4. Letzte 7 Tage
+ seven_days = self.con.execute(
+ """
+ WITH dates AS (
+ SELECT DISTINCT log_date FROM v_nutrition_daily WHERE subject = 'moritz'
+ UNION
+ SELECT DISTINCT log_date FROM v_garmin_daily
+ )
+ SELECT
+ dt.log_date,
+ COALESCE(n.total_kcal, 0) AS intake_kcal,
+ COALESCE(g.total_expenditure_kcal, 0) AS expenditure_kcal,
+ COALESCE(g.active_kcal, 0) AS active_kcal,
+ COALESCE(g.steps, 0) AS steps,
+ CASE
+ WHEN g.total_expenditure_kcal IS NOT NULL THEN (COALESCE(n.total_kcal, 0) - g.total_expenditure_kcal)
+ ELSE NULL
+ END AS balance_kcal,
+ COALESCE(n.total_protein_g, 0.0) AS protein_g,
+ COALESCE(n.total_carbs_g, 0.0) AS carbs_g,
+ COALESCE(n.total_fat_g, 0.0) AS fat_g,
+ g.resting_hr
+ FROM dates dt
+ LEFT JOIN v_nutrition_daily n ON dt.log_date = n.log_date AND n.subject = 'moritz'
+ LEFT JOIN v_garmin_daily g ON dt.log_date = g.log_date
+ WHERE dt.log_date < ?
+ ORDER BY dt.log_date DESC LIMIT 7
+ """,
+ [today_str],
+ ).fetchall()
+
+ seven_days_list = []
+ for r in reversed(seven_days):
+ d_obj = datetime.date.fromisoformat(str(r[0]))
+ seven_days_list.append({
+ "date": str(r[0]),
+ "weekday": WEEKDAYS_DE[d_obj.weekday()],
+ "intake_kcal": round(r[1]) if r[1] is not None else 0,
+ "expenditure_kcal": round(r[2]) if r[2] is not None else 0,
+ "active_kcal": round(r[3]) if r[3] is not None else 0,
+ "steps": int(r[4]) if r[4] is not None else 0,
+ "balance_kcal": round(r[5]) if r[5] is not None else None,
+ "protein_g": round(r[6], 1) if r[6] is not None else 0.0,
+ "carbs_g": round(r[7], 1) if r[7] is not None else 0.0,
+ "fat_g": round(r[8], 1) if r[8] is not None else 0.0,
+ "resting_hr": int(r[9]) if r[9] is not None else None,
+ })
+
+ valid_intake_7 = [r["intake_kcal"] for r in seven_days_list if r["intake_kcal"] > 0]
+ valid_exp_7 = [r["expenditure_kcal"] for r in seven_days_list if r["expenditure_kcal"] > 0]
+ valid_bal_7 = [r["balance_kcal"] for r in seven_days_list if r["balance_kcal"] is not None]
+ valid_steps_7 = [r["steps"] for r in seven_days_list if r["steps"] > 0]
+ valid_prot_7 = [r["protein_g"] for r in seven_days_list if r["protein_g"] > 0]
+
+ avg_intake_7 = round(sum(valid_intake_7) / len(valid_intake_7)) if valid_intake_7 else 0
+ avg_exp_7 = round(sum(valid_exp_7) / len(valid_exp_7)) if valid_exp_7 else 0
+ avg_bal_7 = round(sum(valid_bal_7) / len(valid_bal_7)) if valid_bal_7 else (avg_intake_7 - avg_exp_7)
+ avg_steps_7 = round(sum(valid_steps_7) / len(valid_steps_7)) if valid_steps_7 else 0
+ avg_prot_7 = round(sum(valid_prot_7) / len(valid_prot_7), 1) if valid_prot_7 else 0.0
+
+ # 5. Letzte 30 Tage
+ thirty_days = self.con.execute(
+ """
+ WITH dates AS (
+ SELECT DISTINCT log_date FROM v_nutrition_daily WHERE subject = 'moritz'
+ UNION
+ SELECT DISTINCT log_date FROM v_garmin_daily
+ )
+ SELECT
+ dt.log_date,
+ COALESCE(n.total_kcal, 0) AS intake_kcal,
+ COALESCE(g.total_expenditure_kcal, 0) AS expenditure_kcal,
+ COALESCE(g.active_kcal, 0) AS active_kcal,
+ COALESCE(g.rmr_kcal, 0) AS rmr_kcal,
+ COALESCE(g.steps, 0) AS steps,
+ CASE
+ WHEN g.total_expenditure_kcal IS NOT NULL THEN (COALESCE(n.total_kcal, 0) - g.total_expenditure_kcal)
+ ELSE NULL
+ END AS balance_kcal,
+ CASE
+ WHEN g.total_expenditure_kcal IS NULL THEN 'Garmin ausstehend'
+ WHEN (COALESCE(n.total_kcal, 0) - g.total_expenditure_kcal) < 0 THEN 'Defizit'
+ WHEN (COALESCE(n.total_kcal, 0) - g.total_expenditure_kcal) > 0 THEN 'Überschuss'
+ ELSE 'Ausgeglichen'
+ END AS balance_status,
+ COALESCE(n.total_protein_g, 0.0) AS protein_g,
+ COALESCE(n.total_carbs_g, 0.0) AS carbs_g,
+ COALESCE(n.total_fat_g, 0.0) AS fat_g,
+ g.resting_hr
+ FROM dates dt
+ LEFT JOIN v_nutrition_daily n ON dt.log_date = n.log_date AND n.subject = 'moritz'
+ LEFT JOIN v_garmin_daily g ON dt.log_date = g.log_date
+ WHERE dt.log_date <= ?
+ ORDER BY dt.log_date DESC LIMIT 30
+ """,
+ [today_str],
+ ).fetchall()
+
+ thirty_days_list = []
+ for r in thirty_days:
+ d_obj = datetime.date.fromisoformat(str(r[0]))
+ thirty_days_list.append({
+ "date": str(r[0]),
+ "weekday": WEEKDAYS_DE[d_obj.weekday()],
+ "intake_kcal": round(r[1]) if r[1] is not None else 0,
+ "expenditure_kcal": round(r[2]) if r[2] is not None else 0,
+ "active_kcal": round(r[3]) if r[3] is not None else 0,
+ "rmr_kcal": round(r[4]) if r[4] is not None else 0,
+ "steps": int(r[5]) if r[5] is not None else 0,
+ "balance_kcal": round(r[6]) if r[6] is not None else None,
+ "balance_status": r[7] or ("Defizit" if r[6] and r[6] < 0 else "Überschuss"),
+ "protein_g": round(r[8], 1) if r[8] is not None else 0.0,
+ "carbs_g": round(r[9], 1) if r[9] is not None else 0.0,
+ "fat_g": round(r[10], 1) if r[10] is not None else 0.0,
+ "resting_hr": int(r[11]) if r[11] is not None else None,
+ })
+
+ valid_exp_30 = [r["expenditure_kcal"] for r in thirty_days_list if r["expenditure_kcal"] > 0]
+ valid_intake_30 = [r["intake_kcal"] for r in thirty_days_list if r["intake_kcal"] > 0]
+ valid_bal_30 = [r["balance_kcal"] for r in thirty_days_list if r["balance_kcal"] is not None]
+ valid_steps_30 = [r["steps"] for r in thirty_days_list if r["steps"] > 0]
+
+ avg_intake_30 = round(sum(valid_intake_30) / len(valid_intake_30)) if valid_intake_30 else 0
+ avg_exp_30 = round(sum(valid_exp_30) / len(valid_exp_30)) if valid_exp_30 else 0
+ avg_bal_30 = round(sum(valid_bal_30) / len(valid_bal_30)) if valid_bal_30 else 0
+ total_bal_30 = round(sum(valid_bal_30)) if valid_bal_30 else 0
+ avg_steps_30 = round(sum(valid_steps_30) / len(valid_steps_30)) if valid_steps_30 else 0
+
+ execution_time_ms = round((time.perf_counter() - t0) * 1000, 2)
+ git_status = self.git_sync_manager.status if self.git_sync_manager else "ok"
+ git_conflict = self.git_sync_manager.is_conflict if self.git_sync_manager else False
+
+ return {
+ "status": "ok",
+ "execution_time_ms": execution_time_ms,
+ "git_status": git_status,
+ "git_conflict": git_conflict,
+ "metadata": {
+ "subject": "moritz",
+ "today_date": today_str,
+ "today_pretty": today_pretty,
+ "yesterday_date": yesterday_str,
+ "yesterday_pretty": yesterday_pretty,
+ "sleep_date_label": sleep_date_label,
+ },
+ "today": {
+ "total_kcal": today_kcal,
+ "protein_g": today_protein,
+ "carbs_g": today_carbs,
+ "fat_g": today_fat,
+ "item_count": today_count,
+ "meals": today_meals,
+ },
+ "yesterday": {
+ "intake_kcal": intake_y,
+ "expenditure_kcal": exp_y,
+ "active_kcal": act_y,
+ "rmr_kcal": rmr_y,
+ "steps": steps_y,
+ "balance_kcal": bal_y,
+ "balance_status": status_y,
+ "protein_g": prot_y,
+ "carbs_g": carbs_y,
+ "fat_g": fat_y,
+ "resting_hr": rhr_y,
+ "meals": yesterday_meals,
+ },
+ "garmin_recovery": {
+ "sleep_dur_str": sleep_dur_str,
+ "sleep_score": sleep_score,
+ "sleep_qualifier": sleep_qualifier,
+ "hrv_val": hrv_val,
+ "hrv_status": hrv_status,
+ "bb_change": bb_change,
+ "resting_hr": rhr_val,
+ "deep_minutes": deep_m,
+ "light_minutes": light_m,
+ "rem_minutes": rem_m,
+ "awake_minutes": awake_m,
+ "deep_pct": deep_pct,
+ "light_pct": light_pct,
+ "rem_pct": rem_pct,
+ "awake_pct": awake_pct,
+ },
+ "seven_days": {
+ "days": seven_days_list,
+ "avg_intake_kcal": avg_intake_7,
+ "avg_expenditure_kcal": avg_exp_7,
+ "avg_balance_kcal": avg_bal_7,
+ "avg_steps": avg_steps_7,
+ "avg_protein_g": avg_prot_7,
+ },
+ "thirty_days": {
+ "days": thirty_days_list,
+ "avg_intake_kcal": avg_intake_30,
+ "avg_expenditure_kcal": avg_exp_30,
+ "avg_balance_kcal": avg_bal_30,
+ "total_balance_kcal": total_bal_30,
+ "avg_steps": avg_steps_30,
+ },
+ }
+
+ # --------------------------------------------------------------------------
+ # Mutating Event & Entity Operations
+ # --------------------------------------------------------------------------
+ def _get_target_db(self, kind: str) -> str:
+ if kind.startswith("sleep."):
+ return "sleep"
+ elif kind.startswith("diaper.") or kind.startswith("growth."):
+ return "mila"
+ else:
+ return "nutrition"
+
+ def _sort_jsonl_file(self, file_path: Path, key_fn):
+ if not file_path.is_file():
+ return
+ records = []
+ with open(file_path, "r", encoding="utf-8") as f:
+ for line in f:
+ line = line.strip()
+ if line:
+ try:
+ records.append(json.loads(line))
+ except Exception:
+ pass
+ records.sort(key=key_fn)
+ with open(file_path, "w", encoding="utf-8") as f:
+ for r in records:
+ f.write(json.dumps(r, ensure_ascii=False) + "\n")
+
+ def add_event(self, req: EventCreateRequest) -> Dict[str, Any]:
+ with self._lock:
+ event_id = str(ULID())
+ now_dt = datetime.datetime.now(datetime.timezone.utc)
+ now_iso = now_dt.isoformat()
+
+ if req.since and req.start:
+ raise ValueError("Cannot specify both 'since' and 'start'.")
+
+ if req.date:
+ req.payload["date"] = req.date
+ if not req.start and not req.since:
+ start_dt = datetime.datetime.fromisoformat(f"{req.date}T12:00:00").replace(tzinfo=BERLIN_TZ)
+ start_iso = start_dt.isoformat()
+ elif req.since:
+ delta = parse_duration(req.since)
+ start_dt = (now_dt - delta).astimezone(BERLIN_TZ)
+ start_iso = start_dt.isoformat()
+ else:
+ start_dt = datetime.datetime.fromisoformat(req.start)
+ if start_dt.tzinfo is None:
+ start_dt = start_dt.replace(tzinfo=BERLIN_TZ)
+ start_iso = start_dt.isoformat()
+ elif req.since:
+ delta = parse_duration(req.since)
+ start_dt = (now_dt - delta).astimezone(BERLIN_TZ)
+ start_iso = start_dt.isoformat()
+ elif not req.start:
+ start_dt = now_dt.astimezone(BERLIN_TZ)
+ start_iso = start_dt.isoformat()
+ else:
+ start_dt = datetime.datetime.fromisoformat(req.start)
+ if start_dt.tzinfo is None:
+ start_dt = start_dt.replace(tzinfo=BERLIN_TZ)
+ start_iso = start_dt.isoformat()
+
+ end_iso = None
+ end_dt = None
+ if req.end:
+ end_dt = datetime.datetime.fromisoformat(req.end)
+ if end_dt.tzinfo is None:
+ end_dt = end_dt.replace(tzinfo=BERLIN_TZ)
+ if end_dt < start_dt:
+ raise ValueError(f"End time {req.end} cannot be earlier than start time {req.start}")
+ end_iso = end_dt.isoformat()
+
+ # 15-Minute Duplicate Guard
+ if not req.allow_duplicate:
+ if req.kind in ("nutrition.intake", "feeding.bottle", "feeding.solid"):
+ item_val = req.payload.get("item") or req.payload.get("food_id") or ""
+ req_ml = req.payload.get("consumed_ml")
+ dup_rows = self.con.execute(
+ """
+ SELECT id, kind, subject, start, payload FROM events
+ WHERE subject = ? AND kind = ? AND deleted_at IS NULL
+ AND abs(date_diff('minute', CAST(start AS TIMESTAMPTZ), CAST(? AS TIMESTAMPTZ))) <= 15
+ """,
+ [req.subject, req.kind, start_iso],
+ ).fetchall()
+ for d_id, d_kind, d_subj, d_start, d_payload in dup_rows:
+ dp = json.loads(d_payload) if isinstance(d_payload, str) else (d_payload or {})
+ d_item = dp.get("item") or dp.get("food_id") or ""
+ d_ml = dp.get("consumed_ml")
+ if (item_val and item_val == d_item) or (req_ml is not None and req_ml == d_ml):
+ fb = self._get_event_feedback(req.kind, req.subject, start_dt.date())
+ rec = {"id": d_id, "kind": d_kind, "subject": d_subj, "start": str(d_start), "payload": dp}
+ return {
+ "status": "duplicate_skipped",
+ "id": d_id,
+ "duplicate_guard": "skipped",
+ "event": rec,
+ "record": rec,
+ "feedback": fb,
+ }
+ elif req.kind == "diaper.change":
+ dup_rows = self.con.execute(
+ """
+ SELECT id, kind, subject, start, payload FROM events
+ WHERE subject = ? AND kind = 'diaper.change' AND deleted_at IS NULL
+ AND abs(date_diff('minute', CAST(start AS TIMESTAMPTZ), CAST(? AS TIMESTAMPTZ))) <= 10
+ """,
+ [req.subject, start_iso],
+ ).fetchall()
+ for d_id, d_kind, d_subj, d_start, d_payload in dup_rows:
+ dp = json.loads(d_payload) if isinstance(d_payload, str) else (d_payload or {})
+ if dp.get("pee") == req.payload.get("pee") and dp.get("poo") == req.payload.get("poo"):
+ fb = self._get_event_feedback(req.kind, req.subject, start_dt.date())
+ rec = {"id": d_id, "kind": d_kind, "subject": d_subj, "start": str(d_start), "payload": dp}
+ return {
+ "status": "duplicate_skipped",
+ "id": d_id,
+ "duplicate_guard": "skipped",
+ "event": rec,
+ "record": rec,
+ "feedback": fb,
+ }
+
+ target_db = self._get_target_db(req.kind)
+ month_str = start_dt.astimezone(BERLIN_TZ).strftime("%Y-%m")
+ db_dir = self.data_dir / target_db
+ db_dir.mkdir(parents=True, exist_ok=True)
+ partition_file = db_dir / f"{month_str}.jsonl"
+
+ open_session_warning = None
+ if target_db == "sleep":
+ cal_date = start_dt.astimezone(BERLIN_TZ).strftime("%Y-%m-%d")
+ sleep_type = req.payload.get("type") or "nap"
+ location = req.payload.get("location") or "bett"
+ is_open = (end_iso is None)
+
+ if is_open:
+ open_row = self.con.execute(
+ "SELECT id, startIso FROM sleep WHERE subject = ? AND (isOpen = true OR endIso IS NULL) AND deleted_at IS NULL ORDER BY startIso DESC LIMIT 1",
+ [req.subject],
+ ).fetchone()
+ if open_row:
+ if not req.allow_duplicate:
+ raise ValueError(
+ f"A sleep session is already active for subject '{req.subject}' (id={open_row[0]}, started={open_row[1]}). Stop it first before starting a new one."
+ )
+ else:
+ open_session_warning = {"active_id": open_row[0], "start": str(open_row[1])}
+
+ start_gmt = int(start_dt.timestamp() * 1000)
+ end_gmt = int(end_dt.timestamp() * 1000) if end_dt else None
+ start_local = int((start_dt.timestamp() + start_dt.utcoffset().total_seconds()) * 1000)
+ end_local = int((end_dt.timestamp() + end_dt.utcoffset().total_seconds()) * 1000) if end_dt else None
+ dur_sec = int((end_dt - start_dt).total_seconds()) if end_dt else None
+ nap_sec = dur_sec if (sleep_type == "nap" and dur_sec is not None) else 0
+
+ disk_record = {
+ "id": event_id,
+ "subject": req.subject,
+ "calendarDate": cal_date,
+ "sleepType": sleep_type,
+ "location": location,
+ "isOpen": is_open,
+ "startIso": start_iso,
+ "endIso": end_iso,
+ "sleepStartTimestampGMT": start_gmt,
+ "sleepEndTimestampGMT": end_gmt,
+ "sleepStartTimestampLocal": start_local,
+ "sleepEndTimestampLocal": end_local,
+ "sleepTimeSeconds": dur_sec,
+ "napTimeSeconds": nap_sec,
+ "deepSleepSeconds": None,
+ "lightSleepSeconds": None,
+ "remSleepSeconds": None,
+ "awakeSleepSeconds": None,
+ "awakeCount": None,
+ "avgSleepStress": None,
+ "avgHeartRate": None,
+ "averageSpO2Value": None,
+ "lowestSpO2Value": None,
+ "highestSpO2Value": None,
+ "averageRespirationValue": None,
+ "sleepScores": None,
+ "avgOvernightHrv": None,
+ "hrvStatus": None,
+ "bodyBatteryChange": None,
+ "restingHeartRate": None,
+ "restlessMomentsCount": None,
+ "note": req.note,
+ "actor": req.actor,
+ "created_at": now_iso,
+ "updated_at": None,
+ "deleted_at": None,
+ }
+ self.con.execute(
+ """
+ INSERT INTO sleep (
+ id, subject, calendarDate, sleepType, location, isOpen,
+ startIso, endIso, sleepStartTimestampGMT, sleepEndTimestampGMT,
+ sleepStartTimestampLocal, sleepEndTimestampLocal,
+ sleepTimeSeconds, napTimeSeconds, note, actor, created_at, updated_at, deleted_at
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ """,
+ [
+ event_id, req.subject, cal_date, sleep_type, location, is_open,
+ start_iso, end_iso, start_gmt, end_gmt, start_local, end_local,
+ dur_sec, nap_sec, req.note, req.actor, now_iso, None, None,
+ ],
+ )
+ sort_key = lambda x: (x.get("calendarDate", ""), x.get("startIso", ""), x.get("id", ""))
+ elif target_db == "mila":
+ disk_record = {
+ "id": event_id,
+ "kind": req.kind,
+ "subject": req.subject,
+ "actor": req.actor,
+ "start": start_iso,
+ "end": end_iso,
+ "note": req.note,
+ "payload": req.payload,
+ "source_ref": req.source_ref,
+ "created_at": now_iso,
+ "updated_at": None,
+ "deleted_at": None,
+ }
+ self.con.execute(
+ """
+ INSERT INTO mila (id, kind, subject, actor, start, "end", note, payload, source_ref, created_at, updated_at, deleted_at)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ """,
+ [event_id, req.kind, req.subject, req.actor, start_iso, end_iso, req.note, json.dumps(req.payload), req.source_ref, now_iso, None, None],
+ )
+ sort_key = lambda x: (x.get("start", ""), x.get("id", ""))
+ else:
+ disk_record = {
+ "id": event_id,
+ "kind": req.kind,
+ "subject": req.subject,
+ "actor": req.actor,
+ "start": start_iso,
+ "end": end_iso,
+ "estimated": req.estimated,
+ "meal": req.meal,
+ "note": req.note,
+ "payload": req.payload,
+ "source_ref": req.source_ref,
+ "created_at": now_iso,
+ "updated_at": None,
+ "deleted_at": None,
+ }
+ self.con.execute(
+ """
+ INSERT INTO nutrition (id, kind, subject, actor, start, "end", estimated, meal, note, payload, source_ref, created_at, updated_at, deleted_at)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ """,
+ [event_id, req.kind, req.subject, req.actor, start_iso, end_iso, req.estimated, req.meal, req.note, json.dumps(req.payload), req.source_ref, now_iso, None, None],
+ )
+ sort_key = lambda x: (x.get("start", ""), x.get("id", ""))
+
+ with open(partition_file, "a", encoding="utf-8") as f:
+ f.write(json.dumps(disk_record, ensure_ascii=False) + "\n")
+
+ self._sort_jsonl_file(partition_file, key_fn=sort_key)
+
+ if self.git_sync_manager:
+ self.git_sync_manager.commit_and_push(f"chore(data): log {req.kind} for {req.subject}")
+
+ feedback = self._get_event_feedback(req.kind, req.subject, start_dt.date())
+ res = {
+ "status": "success",
+ "id": event_id,
+ "event": disk_record,
+ "record": disk_record,
+ "feedback": feedback,
+ }
+ if open_session_warning:
+ res["open_session_warning"] = open_session_warning
+ return res
+
+ def stop_event(self, req: EventStopRequest) -> Dict[str, Any]:
+ with self._lock:
+ target_db = self._get_target_db(req.kind)
+ now_dt = datetime.datetime.now(datetime.timezone.utc)
+
+ if req.since and req.end_time:
+ raise ValueError("Cannot specify both 'since' and 'end_time'.")
+
+ if req.since:
+ delta = parse_duration(req.since)
+ end_dt = (now_dt - delta).astimezone(BERLIN_TZ)
+ elif req.end_time:
+ end_dt = datetime.datetime.fromisoformat(req.end_time)
+ if end_dt.tzinfo is None:
+ end_dt = end_dt.replace(tzinfo=BERLIN_TZ)
+ else:
+ end_dt = now_dt.astimezone(BERLIN_TZ)
+
+ if target_db == "sleep":
+ if req.event_id:
+ row = self.con.execute(
+ """
+ SELECT id, startIso, sleepType FROM sleep
+ WHERE id = ? AND subject = ? AND (isOpen = true OR endIso IS NULL) AND deleted_at IS NULL
+ """,
+ [req.event_id, req.subject],
+ ).fetchone()
+ else:
+ row = self.con.execute(
+ """
+ SELECT id, startIso, sleepType FROM sleep
+ WHERE subject = ? AND (isOpen = true OR endIso IS NULL) AND deleted_at IS NULL
+ ORDER BY startIso DESC LIMIT 1
+ """,
+ [req.subject],
+ ).fetchone()
+ if not row:
+ raise ValueError(f"No active open sleep session found for subject '{req.subject}'.")
+ resolved_id, start_str, sleep_type = row[0], str(row[1]), row[2]
+ else:
+ if req.event_id:
+ row = self.con.execute(
+ f"""
+ SELECT id, start FROM "{target_db}"
+ WHERE id = ? AND kind = ? AND subject = ? AND "end" IS NULL AND deleted_at IS NULL
+ """,
+ [req.event_id, req.kind, req.subject],
+ ).fetchone()
+ else:
+ row = self.con.execute(
+ f"""
+ SELECT id, start FROM "{target_db}"
+ WHERE kind = ? AND subject = ? AND "end" IS NULL AND deleted_at IS NULL
+ ORDER BY start DESC LIMIT 1
+ """,
+ [req.kind, req.subject],
+ ).fetchone()
+ if not row:
+ raise ValueError(f"No active open event found for kind='{req.kind}' and subject='{req.subject}'.")
+ resolved_id, start_str, sleep_type = row[0], str(row[1]), None
+
+ start_dt = datetime.datetime.fromisoformat(start_str.replace("Z", "+00:00"))
+ if start_dt.tzinfo is None:
+ start_dt = start_dt.replace(tzinfo=BERLIN_TZ)
+ if end_dt < start_dt:
+ raise ValueError(f"End time {end_dt.isoformat()} cannot be earlier than start time {start_dt.isoformat()}")
+
+ end_iso = end_dt.isoformat()
+ now_iso = now_dt.isoformat()
+ dur_sec = int((end_dt - start_dt).total_seconds())
+ dur_min = dur_sec // 60
+
+ if target_db == "sleep":
+ end_gmt = int(end_dt.timestamp() * 1000)
+ end_local = int((end_dt.timestamp() + end_dt.utcoffset().total_seconds()) * 1000)
+ nap_sec = dur_sec if sleep_type == "nap" else 0
+ self.con.execute(
+ """
+ UPDATE sleep
+ SET endIso = ?, isOpen = false, sleepEndTimestampGMT = ?, sleepEndTimestampLocal = ?,
+ sleepTimeSeconds = ?, napTimeSeconds = ?, updated_at = ?
+ WHERE id = ?
+ """,
+ [end_iso, end_gmt, end_local, dur_sec, nap_sec, now_iso, resolved_id],
+ )
+ self._update_jsonl_record("sleep", resolved_id, {
+ "endIso": end_iso,
+ "isOpen": False,
+ "sleepEndTimestampGMT": end_gmt,
+ "sleepEndTimestampLocal": end_local,
+ "sleepTimeSeconds": dur_sec,
+ "napTimeSeconds": nap_sec,
+ "updated_at": now_iso,
+ })
+ else:
+ self.con.execute(
+ f'UPDATE "{target_db}" SET "end" = ?, updated_at = ? WHERE id = ?',
+ [end_iso, now_iso, resolved_id],
+ )
+ self._update_jsonl_record(target_db, resolved_id, {"end": end_iso, "updated_at": now_iso})
+
+ if self.git_sync_manager:
+ self.git_sync_manager.commit_and_push(f"chore(data): stop {req.kind} for {req.subject}")
+
+ feedback = self._get_event_feedback(req.kind, req.subject, end_dt.date())
+ return {
+ "status": "success",
+ "id": resolved_id,
+ "duration_minutes": dur_min,
+ "end": end_iso,
+ "feedback": feedback,
+ }
+
+ def delete_event(self, event_id: str) -> Dict[str, Any]:
+ with self._lock:
+ now_iso = datetime.datetime.now(datetime.timezone.utc).isoformat()
+ row = self.con.execute("SELECT kind, subject FROM events WHERE id = ?", [event_id]).fetchone()
+ if not row:
+ raise ValueError(f"Event with id '{event_id}' not found.")
+
+ kind, subject = row[0], row[1]
+ target_db = self._get_target_db(kind)
+
+ self.con.execute(f'UPDATE "{target_db}" SET deleted_at = ? WHERE id = ?', [now_iso, event_id])
+ self._update_jsonl_record(target_db, event_id, {"deleted_at": now_iso})
+
+ if self.git_sync_manager:
+ self.git_sync_manager.commit_and_push(f"chore(data): delete event {event_id} ({kind})")
+
+ return {"status": "success", "id": event_id}
+
+ def put_entity(self, req: EntityPutRequest) -> Dict[str, Any]:
+ with self._lock:
+ if req.collection not in ["foods", "recipes", "places", "people"]:
+ raise ValueError(f"Unknown entity collection: {req.collection}")
+
+ entity_id = req.record.get("id")
+ if not entity_id:
+ raise ValueError("Entity record must have an 'id' field.")
+
+ entities_dir = self.data_dir / "entities"
+ entities_dir.mkdir(parents=True, exist_ok=True)
+ target_file = entities_dir / f"{req.collection}.jsonl"
+
+ existing_records = []
+ replaced = False
+ if target_file.is_file():
+ with open(target_file, "r", encoding="utf-8") as f:
+ for line in f:
+ line = line.strip()
+ if line:
+ try:
+ r = json.loads(line)
+ if r.get("id") == entity_id:
+ existing_records.append(req.record)
+ replaced = True
+ else:
+ existing_records.append(r)
+ except Exception:
+ pass
+
+ if not replaced:
+ existing_records.append(req.record)
+
+ existing_records.sort(key=lambda x: str(x.get("id", "")))
+
+ with open(target_file, "w", encoding="utf-8") as f:
+ for r in existing_records:
+ f.write(json.dumps(r, ensure_ascii=False) + "\n")
+
+ columns = list(req.record.keys())
+ placeholders = ", ".join(["?"] * len(columns))
+ col_names = ", ".join([f'"{c}"' for c in columns])
+ values = [
+ json.dumps(req.record[c]) if isinstance(req.record[c], (dict, list)) else req.record[c]
+ for c in columns
+ ]
+ self.con.execute(
+ f'INSERT OR REPLACE INTO "{req.collection}" ({col_names}) VALUES ({placeholders})',
+ values,
+ )
+
+ if self.git_sync_manager:
+ self.git_sync_manager.commit_and_push(f"chore(data): update entity {req.collection}/{entity_id}")
+
+ return {"status": "success", "collection": req.collection, "id": entity_id}
+
+ def _update_jsonl_record(self, target_db: str, event_id: str, updates: Dict[str, Any]):
+ db_dir = self.data_dir / target_db
+ if not db_dir.is_dir():
+ return
+
+ for p_file in db_dir.glob("*.jsonl"):
+ records = []
+ found = False
+ with open(p_file, "r", encoding="utf-8") as f:
+ for line in f:
+ line = line.strip()
+ if not line:
+ continue
+ try:
+ r = json.loads(line)
+ if r.get("id") == event_id:
+ r.update(updates)
+ found = True
+ records.append(r)
+ except Exception:
+ pass
+ if found:
+ with open(p_file, "w", encoding="utf-8") as f:
+ for r in records:
+ f.write(json.dumps(r, ensure_ascii=False) + "\n")
+ return
+
+ def _get_event_feedback(self, kind: str, subject: str, target_date: datetime.date) -> Dict[str, Any]:
+ """Generates compact running status feedback after an insert or stop."""
+ date_str = target_date.strftime("%Y-%m-%d")
+ if kind.startswith("nutrition") and subject != "mila":
+ daily = self.con.execute(
+ "SELECT total_protein_g, total_carbs_g, total_sugar_g, total_fat_g, total_kcal FROM v_nutrition_daily WHERE subject = ? AND log_date = ?",
+ [subject, date_str],
+ ).fetchone()
+ if daily:
+ return {
+ "day_total_formatted": f"Tag bisher ({date_str}): ~{daily[0]} g Eiweiß · ~{daily[1]} g KH (davon ~{daily[2]} g Zucker) · ~{daily[3]} g Fett · ~{int(daily[4])} kcal",
+ "protein_g": daily[0],
+ "carbs_g": daily[1],
+ "kcal": daily[4],
+ }
+ elif kind.startswith("sleep"):
+ daily = self.con.execute(
+ "SELECT total_sleep_hours, nap_count, night_sleep_minutes FROM v_sleep_daily WHERE subject = ? AND log_date = ?",
+ [subject, date_str],
+ ).fetchone()
+ if daily:
+ return {
+ "day_total_formatted": f"Schlaf ({date_str}): {daily[0]} h gesamt ({daily[1]} Schläfchen, {daily[2]} min Nacht)",
+ "total_hours": daily[0],
+ "nap_count": daily[1],
+ }
+ elif kind.startswith("diaper"):
+ daily = self.con.execute(
+ "SELECT total_changes, wet_count, dirty_count FROM v_diaper_daily WHERE subject = ? AND log_date = ?",
+ [subject, date_str],
+ ).fetchone()
+ if daily:
+ return {
+ "day_total_formatted": f"Windeln ({date_str}): {daily[0]} gewechselt ({daily[1]} Pipi, {daily[2]} Stuhl)",
+ "total_changes": daily[0],
+ "wet_count": daily[1],
+ "dirty_count": daily[2],
+ }
+ elif kind.startswith("feeding") or (kind.startswith("nutrition") and subject == "mila"):
+ daily = self.con.execute(
+ "SELECT bottle_count, total_consumed_ml, solid_count, total_solid_g, total_kcal FROM v_feeding_daily WHERE subject = ? AND log_date = ?",
+ [subject, date_str],
+ ).fetchone()
+ if daily:
+ b_cnt, b_ml, s_cnt, s_g, tot_kcal = daily[0], daily[1], daily[2], daily[3], daily[4]
+ parts = []
+ if b_cnt > 0 or b_ml > 0:
+ parts.append(f"{b_cnt} Fläschchen ({int(b_ml)} ml getrunken)")
+ if s_cnt > 0 or s_g > 0:
+ parts.append(f"{s_cnt}x Brei/Beikost ({int(s_g)} g)")
+ summary_str = " · ".join(parts) if parts else f"{b_cnt} Fläschchen ({int(b_ml)} ml getrunken)"
+ return {
+ "day_total_formatted": f"Ernährung Mila ({date_str}): {summary_str} · ~{int(tot_kcal)} kcal",
+ "bottle_count": b_cnt,
+ "total_consumed_ml": b_ml,
+ "solid_count": s_cnt,
+ "total_solid_g": s_g,
+ "total_kcal": tot_kcal,
+ }
+ elif kind.startswith("growth"):
+ row = self.con.execute(
+ "SELECT weight_kg, height_cm, head_circ_cm FROM v_growth WHERE subject = ? AND log_date = ? ORDER BY start DESC LIMIT 1",
+ [subject, date_str],
+ ).fetchone()
+ if row:
+ w_kg, h_cm, hc_cm = row[0], row[1], row[2]
+ p_row = self.con.execute("SELECT birthdate FROM people WHERE id = ?", [subject]).fetchone()
+ bdate = p_row[0] if p_row and p_row[0] else datetime.date(2026, 3, 14)
+ if isinstance(bdate, str):
+ bdate = datetime.date.fromisoformat(bdate)
+ age_days = (target_date - bdate).days
+ age_months = max(0.0, age_days / 30.4375)
+ try:
+ from app.who_growth import format_growth_feedback, calculate_percentile
+ fb_text = format_growth_feedback(w_kg, h_cm, age_months, date_str)
+ fb_res: Dict[str, Any] = {
+ "day_total_formatted": fb_text,
+ "age_months": round(age_months, 2),
+ }
+ if w_kg is not None:
+ pw = calculate_percentile("weight", w_kg, age_months)
+ fb_res["weight_kg"] = w_kg
+ fb_res["weight_percentile"] = pw["percentile"]
+ fb_res["weight_percentile_str"] = pw["percentile_str"]
+ if h_cm is not None:
+ ph = calculate_percentile("height", h_cm, age_months)
+ fb_res["height_cm"] = h_cm
+ fb_res["height_percentile"] = ph["percentile"]
+ fb_res["height_percentile_str"] = ph["percentile_str"]
+ return fb_res
+ except Exception as e:
+ logger.warning(f"Error computing growth feedback: {e}")
+ return {
+ "day_total_formatted": f"Wachstum ({date_str}): Gewicht {w_kg} kg, Größe {h_cm} cm",
+ "weight_kg": w_kg,
+ "height_cm": h_cm,
+ }
+ return {}
+
+ def get_mila_day_detail(self, target_date: str) -> Dict[str, Any]:
+ """Returns the full day breakdown for Mila on target_date YYYY-MM-DD."""
+ return self.get_mila_dashboard(as_of_date=target_date)
+
+ def get_moritz_day_detail(self, target_date: str) -> Dict[str, Any]:
+ """Returns the full day breakdown for Moritz on target_date YYYY-MM-DD."""
+ return self.get_moritz_dashboard(as_of_date=target_date)
+
+ def get_summary(self, subject: str, date_str: Optional[str] = None) -> Dict[str, Any]:
+ """Returns daily summary for subject (nutrition, sleep, deficit, diaper, feeding, growth)."""
+ with self._lock:
+ target_date = date_str or datetime.datetime.now(BERLIN_TZ).strftime("%Y-%m-%d")
+ res: Dict[str, Any] = {"subject": subject, "date": target_date}
+
+ nut = self.query(f"SELECT * FROM v_nutrition_daily WHERE subject = '{subject}' AND log_date = '{target_date}'")
+ if nut:
+ res["nutrition"] = nut[0]
+ else:
+ res["nutrition"] = {
+ "total_protein_g": 0.0,
+ "total_carbs_g": 0.0,
+ "total_sugar_g": 0.0,
+ "total_fat_g": 0.0,
+ "total_fiber_g": 0.0,
+ "total_salt_g": 0.0,
+ "total_kcal": 0,
+ "item_count": 0,
+ }
+
+ slp = self.query(f"SELECT * FROM v_sleep_daily WHERE subject = '{subject}' AND log_date = '{target_date}'")
+ if slp:
+ res["sleep"] = slp[0]
+
+ open_sleep = self.query(
+ f"SELECT id, start, local_start, sleep_type, location FROM v_sleep WHERE subject = '{subject}' AND is_open = true"
+ )
+ if open_sleep:
+ res["open_sleep"] = open_sleep[0]
+
+ dia = self.query(f"SELECT * FROM v_diaper_daily WHERE subject = '{subject}' AND log_date = '{target_date}'")
+ if dia:
+ res["diaper"] = dia[0]
+
+ fee = self.query(f"SELECT * FROM v_feeding_daily WHERE subject = '{subject}' AND log_date = '{target_date}'")
+ if fee:
+ res["feeding"] = fee[0]
+
+ grw = self.query(f"SELECT * FROM v_growth WHERE subject = '{subject}' ORDER BY start DESC LIMIT 1")
+ if grw:
+ res["growth_latest"] = grw[0]
+
+ return res
+
+ def find_places_near(
+ self,
+ lat: float,
+ lon: float,
+ radius_m: float = 1000.0,
+ category: Optional[str] = None,
+ ) -> List[Dict[str, Any]]:
+ """Radius search for places using DuckDB distance_m macro."""
+ with self._lock:
+ cat_filter = f"AND category = '{category}'" if category else ""
+ sql = f"""
+ SELECT id, name, category, lat, lon, address, google_maps_url, tags, notes, documents,
+ ROUND(distance_m({lat}, {lon}, lat, lon), 1) AS distance_meters
+ FROM places
+ WHERE distance_m({lat}, {lon}, lat, lon) <= {radius_m}
+ {cat_filter}
+ ORDER BY distance_meters ASC
+ """
+ res = self.con.execute(sql).fetchall()
+ col_names = [desc[0] for desc in self.con.description]
+ results = []
+ for row in res:
+ rec = {}
+ for c, val in zip(col_names, row):
+ if isinstance(val, str) and (val.startswith("{") or val.startswith("[")):
+ try:
+ rec[c] = json.loads(val)
+ except Exception:
+ rec[c] = val
+ else:
+ rec[c] = val
+ results.append(rec)
+ return results
+
+ def list_entities(self, collection: str) -> List[Dict[str, Any]]:
+ """Lists all entity records in the specified collection."""
+ with self._lock:
+ if collection not in ["foods", "recipes", "places", "people"]:
+ raise ValueError(f"Unknown entity collection: {collection}")
+ res = self.con.execute(f'SELECT * FROM "{collection}"').fetchall()
+ col_names = [desc[0] for desc in self.con.description]
+ results = []
+ for row in res:
+ rec = {}
+ for c, val in zip(col_names, row):
+ if isinstance(val, str) and (val.startswith("{") or val.startswith("[")):
+ try:
+ rec[c] = json.loads(val)
+ except Exception:
+ rec[c] = val
+ else:
+ rec[c] = val
+ results.append(rec)
+ return results
+
+ def sync_garmin(self, req: GarminSyncRequest) -> Dict[str, Any]:
+ """Mirrors Garmin daily, sleep, and activity summaries to jsonl and reloads DuckDB."""
+ with self._lock:
+ garmin_dir = self.data_dir / "garmin"
+ daily_dir = garmin_dir / "daily"
+ sleep_dir = garmin_dir / "sleep"
+ act_dir = garmin_dir / "activities"
+ unified_sleep_dir = self.data_dir / "sleep"
+
+ daily_dir.mkdir(parents=True, exist_ok=True)
+ sleep_dir.mkdir(parents=True, exist_ok=True)
+ act_dir.mkdir(parents=True, exist_ok=True)
+ unified_sleep_dir.mkdir(parents=True, exist_ok=True)
+
+ def _read_existing(file_path: Path) -> List[Dict[str, Any]]:
+ if not file_path.is_file():
+ return []
+ items = []
+ with open(file_path, "r", encoding="utf-8") as f:
+ for line in f:
+ if line.strip():
+ try:
+ items.append(json.loads(line))
+ except Exception:
+ pass
+ return items
+
+ def _write_atomic(file_path: Path, items: List[Dict[str, Any]]):
+ tmp_path = file_path.with_suffix(".tmp")
+ with open(tmp_path, "w", encoding="utf-8") as f:
+ for item in items:
+ f.write(json.dumps(item, ensure_ascii=False) + "\n")
+ tmp_path.replace(file_path)
+
+ # 1. Upsert daily summaries
+ for summary in req.daily:
+ cal_date = summary.get("calendarDate")
+ if not cal_date:
+ continue
+ month = cal_date[:7]
+ target_file = daily_dir / f"{month}.jsonl"
+ existing = _read_existing(target_file)
+ id_map = {item.get("calendarDate"): item for item in existing if item.get("calendarDate")}
+ id_map[cal_date] = summary
+ sorted_items = sorted(id_map.values(), key=lambda x: str(x.get("calendarDate", "")))
+ _write_atomic(target_file, sorted_items)
+
+ # 2. Upsert sleep data
+ for sleep_data in req.sleep:
+ dto = sleep_data.get("dailySleepDTO") if isinstance(sleep_data.get("dailySleepDTO"), dict) else {}
+ cal_date = dto.get("calendarDate") or sleep_data.get("calendarDate")
+ if not cal_date:
+ continue
+ month = cal_date[:7]
+ target_file = sleep_dir / f"{month}.jsonl"
+ existing = _read_existing(target_file)
+
+ def _get_date(x):
+ d = (x.get("dailySleepDTO") or {}).get("calendarDate") if isinstance(x.get("dailySleepDTO"), dict) else None
+ return str(d or x.get("calendarDate") or "")
+
+ id_map = {_get_date(item): item for item in existing if _get_date(item)}
+ id_map[cal_date] = sleep_data
+ sorted_items = sorted(id_map.values(), key=_get_date)
+ _write_atomic(target_file, sorted_items)
+
+ # Unified sleep record for Moritz
+ start_gmt = dto.get("sleepStartTimestampGMT")
+ end_gmt = dto.get("sleepEndTimestampGMT")
+ if start_gmt:
+ start_dt = datetime.datetime.fromtimestamp(start_gmt / 1000, datetime.timezone.utc)
+ start_iso = start_dt.isoformat()
+ else:
+ start_iso = f"{cal_date}T22:30:00+02:00"
+
+ if end_gmt:
+ end_dt = datetime.datetime.fromtimestamp(end_gmt / 1000, datetime.timezone.utc)
+ end_iso = end_dt.isoformat()
+ else:
+ end_iso = f"{cal_date}T06:30:00+02:00"
+
+ nap_sec = dto.get("napTimeSeconds") or 0
+ sleep_sec = dto.get("sleepTimeSeconds") or 0
+ sleep_type = "nap" if (nap_sec > 0 and sleep_sec == 0) else "night"
+
+ unified_rec = {
+ "id": str(dto.get("id") or (int(start_gmt) if start_gmt else cal_date)),
+ "subject": "moritz",
+ "calendarDate": cal_date,
+ "sleepType": sleep_type,
+ "location": "bett",
+ "isOpen": False,
+ "startIso": start_iso,
+ "endIso": end_iso,
+ "sleepStartTimestampGMT": dto.get("sleepStartTimestampGMT"),
+ "sleepEndTimestampGMT": dto.get("sleepEndTimestampGMT"),
+ "sleepStartTimestampLocal": dto.get("sleepStartTimestampLocal"),
+ "sleepEndTimestampLocal": dto.get("sleepEndTimestampLocal"),
+ "sleepTimeSeconds": dto.get("sleepTimeSeconds"),
+ "napTimeSeconds": nap_sec,
+ "deepSleepSeconds": dto.get("deepSleepSeconds"),
+ "lightSleepSeconds": dto.get("lightSleepSeconds"),
+ "remSleepSeconds": dto.get("remSleepSeconds"),
+ "awakeSleepSeconds": dto.get("awakeSleepSeconds"),
+ "awakeCount": dto.get("awakeCount"),
+ "avgSleepStress": dto.get("avgSleepStress"),
+ "avgHeartRate": dto.get("avgHeartRate"),
+ "averageSpO2Value": dto.get("averageSpO2Value"),
+ "lowestSpO2Value": dto.get("lowestSpO2Value"),
+ "highestSpO2Value": dto.get("highestSpO2Value"),
+ "averageRespirationValue": dto.get("averageRespirationValue"),
+ "sleepScores": dto.get("sleepScores"),
+ "avgOvernightHrv": sleep_data.get("avgOvernightHrv"),
+ "hrvStatus": sleep_data.get("hrvStatus"),
+ "bodyBatteryChange": sleep_data.get("bodyBatteryChange"),
+ "restingHeartRate": sleep_data.get("restingHeartRate"),
+ "restlessMomentsCount": sleep_data.get("restlessMomentsCount"),
+ "note": None,
+ "actor": "garmin_sync",
+ "created_at": start_iso,
+ "updated_at": None,
+ "deleted_at": None,
+ }
+
+ unified_file = unified_sleep_dir / f"{month}.jsonl"
+ existing_u = _read_existing(unified_file)
+ new_u = [it for it in existing_u if not (it.get("subject") == "moritz" and it.get("calendarDate") == cal_date)]
+ new_u.append(unified_rec)
+ new_u.sort(key=lambda x: (str(x.get("calendarDate", "")), str(x.get("startIso", "")), str(x.get("id", ""))))
+ _write_atomic(unified_file, new_u)
+
+ # 3. Upsert activities
+ if req.activities:
+ by_month: Dict[str, List[Dict[str, Any]]] = {}
+ for act in req.activities:
+ start_local = act.get("startTimeLocal", "")
+ m = start_local[:7] if len(start_local) >= 7 else datetime.date.today().strftime("%Y-%m")
+ by_month.setdefault(m, []).append(act)
+
+ for month, act_list in by_month.items():
+ target_file = act_dir / f"{month}.jsonl"
+ existing = _read_existing(target_file)
+ id_map = {item.get("activityId"): item for item in existing if item.get("activityId")}
+ for act in act_list:
+ aid = act.get("activityId")
+ if aid:
+ id_map[aid] = act
+ sorted_acts = sorted(id_map.values(), key=lambda x: str(x.get("startTimeLocal", "")))
+ _write_atomic(target_file, sorted_acts)
+
+ # Reload all DuckDB tables
+ self.reload_all()
+
+ if self.git_sync_manager:
+ self.git_sync_manager.commit_and_push("chore(garmin): sync daily/sleep/activities data")
+
+ return {
+ "status": "success",
+ "synced_daily": len(req.daily),
+ "synced_sleep": len(req.sleep),
+ "synced_activities": len(req.activities),
+ }
diff --git a/projects/duckdb-backend/app/git_sync.py b/projects/duckdb-backend/app/git_sync.py
new file mode 100644
index 0000000..ebf2eb1
--- /dev/null
+++ b/projects/duckdb-backend/app/git_sync.py
@@ -0,0 +1,326 @@
+from __future__ import annotations
+
+import asyncio
+import datetime
+import logging
+import os
+import subprocess
+import threading
+import urllib.parse
+from pathlib import Path
+from typing import Callable, Optional
+
+from app.config import (
+ AUTO_GIT_SYNC,
+ GIT_AUTHOR_EMAIL,
+ GIT_AUTHOR_NAME,
+ GIT_BRANCH,
+ GIT_PASSWORD,
+ GIT_REPO_URL,
+ GIT_SYNC_INTERVAL_SECONDS,
+ GIT_USERNAME,
+ WIKI_DIR,
+)
+
+logger = logging.getLogger("duckdb_backend.git_sync")
+
+
+class GitSyncManager:
+ def __init__(self, wiki_dir: Optional[Path] = None, on_data_pulled: Optional[Callable[[], None]] = None):
+ self.wiki_dir = wiki_dir or WIKI_DIR
+ self.on_data_pulled = on_data_pulled
+ self._lock = threading.Lock()
+ self.status = "ok"
+ self.is_conflict = False
+ self.last_sync_time: Optional[str] = None
+ self.last_commit_hash: Optional[str] = None
+ self.last_error: Optional[str] = None
+ self._bg_task: Optional[asyncio.Task] = None
+ self._stop_event = asyncio.Event()
+
+ def _configure_git_client(self):
+ """Configures git client with safe.directory and author identity."""
+ try:
+ subprocess.run(["git", "config", "--global", "http.sslVerify", "false"], check=False)
+ subprocess.run(["git", "config", "--global", "--add", "safe.directory", "*"], check=False)
+ if (self.wiki_dir / ".git").is_dir():
+ subprocess.run(["git", "-C", str(self.wiki_dir), "config", "user.name", GIT_AUTHOR_NAME], check=False)
+ subprocess.run(["git", "-C", str(self.wiki_dir), "config", "user.email", GIT_AUTHOR_EMAIL], check=False)
+ except Exception as e:
+ logger.warning(f"Failed to configure git client options: {e}")
+
+ def get_auth_repo_url(self) -> str:
+ if GIT_USERNAME and GIT_PASSWORD and "://" in GIT_REPO_URL:
+ proto, rest = GIT_REPO_URL.split("://", 1)
+ # Remove existing credentials in URL if any
+ if "@" in rest:
+ rest = rest.split("@", 1)[1]
+ user_enc = urllib.parse.quote(GIT_USERNAME, safe="")
+ pass_enc = urllib.parse.quote(GIT_PASSWORD, safe="")
+ return f"{proto}://{user_enc}:{pass_enc}@{rest}"
+ return GIT_REPO_URL
+
+ def ensure_repo(self) -> bool:
+ """Ensures repository exists and is initialized."""
+ with self._lock:
+ self._configure_git_client()
+ if (self.wiki_dir / ".git").is_dir():
+ try:
+ res = subprocess.run(
+ ["git", "-C", str(self.wiki_dir), "rev-parse", "--short", "HEAD"],
+ capture_output=True,
+ text=True,
+ check=False,
+ )
+ if res.returncode == 0:
+ self.last_commit_hash = res.stdout.strip()
+ return True
+ except Exception as e:
+ logger.warning(f"Failed to check git rev: {e}")
+ return True
+
+ if not GIT_REPO_URL:
+ logger.info(f"Wiki directory {self.wiki_dir} is not a git repo, but no GIT_REPO_URL configured.")
+ return False
+
+ logger.info(f"Initializing repository {GIT_REPO_URL} in {self.wiki_dir}...")
+ self.wiki_dir.mkdir(parents=True, exist_ok=True)
+ auth_url = self.get_auth_repo_url()
+ try:
+ # Use git init + remote + fetch + checkout to safely support non-empty directories (e.g. lost+found on PVC)
+ subprocess.run(["git", "-C", str(self.wiki_dir), "init"], capture_output=True, check=False)
+ self._configure_git_client()
+ subprocess.run(
+ ["git", "-C", str(self.wiki_dir), "remote", "add", "origin", auth_url],
+ capture_output=True,
+ check=False,
+ )
+ subprocess.run(
+ ["git", "-C", str(self.wiki_dir), "remote", "set-url", "origin", auth_url],
+ capture_output=True,
+ check=False,
+ )
+ res = subprocess.run(
+ ["git", "-C", str(self.wiki_dir), "fetch", "origin", GIT_BRANCH],
+ capture_output=True,
+ text=True,
+ check=False,
+ timeout=60,
+ )
+ if res.returncode == 0:
+ if (self.wiki_dir / ".git" / "shallow").exists():
+ subprocess.run(
+ ["git", "-C", str(self.wiki_dir), "fetch", "--unshallow", "origin", GIT_BRANCH],
+ capture_output=True,
+ check=False,
+ )
+ checkout_res = subprocess.run(
+ ["git", "-C", str(self.wiki_dir), "checkout", "-f", "-B", GIT_BRANCH, f"origin/{GIT_BRANCH}"],
+ capture_output=True,
+ text=True,
+ check=False,
+ )
+ if checkout_res.returncode == 0:
+ logger.info("Successfully initialized and checked out wiki repository.")
+ self.status = "ok"
+ self.is_conflict = False
+ self.last_sync_time = datetime.datetime.now(datetime.timezone.utc).isoformat()
+ return True
+ else:
+ self.status = "error"
+ self.last_error = f"Checkout failed: {checkout_res.stderr.strip()}"
+ logger.error(self.last_error)
+ return False
+ else:
+ self.status = "error"
+ self.last_error = f"Fetch failed: {res.stderr.strip()}"
+ logger.error(self.last_error)
+ return False
+ except Exception as e:
+ self.status = "error"
+ self.last_error = f"Clone exception: {e}"
+ logger.error(self.last_error)
+ return False
+
+ def pull_rebase(self) -> bool:
+ """Pulls latest changes via rebase. Handles conflicts cleanly."""
+ if not (self.wiki_dir / ".git").is_dir():
+ return False
+
+ with self._lock:
+ try:
+ # Update remote url with credentials if provided
+ auth_url = self.get_auth_repo_url()
+ subprocess.run(
+ ["git", "-C", str(self.wiki_dir), "remote", "set-url", "origin", auth_url],
+ capture_output=True,
+ check=False,
+ )
+
+ # Fetch and rebase with autostash
+ res = subprocess.run(
+ ["git", "-C", str(self.wiki_dir), "pull", "--rebase", "--autostash", "origin", GIT_BRANCH],
+ capture_output=True,
+ text=True,
+ check=False,
+ timeout=30,
+ )
+ if res.returncode == 0:
+ self.status = "ok"
+ self.is_conflict = False
+ self.last_error = None
+ self.last_sync_time = datetime.datetime.now(datetime.timezone.utc).isoformat()
+ # Update commit hash
+ rev = subprocess.run(
+ ["git", "-C", str(self.wiki_dir), "rev-parse", "--short", "HEAD"],
+ capture_output=True,
+ text=True,
+ check=False,
+ )
+ if rev.returncode == 0:
+ self.last_commit_hash = rev.stdout.strip()
+
+ if self.on_data_pulled:
+ try:
+ self.on_data_pulled()
+ except Exception as ex:
+ logger.error(f"Error in on_data_pulled callback: {ex}")
+ return True
+ else:
+ # Check if conflict occurred
+ stderr = res.stderr or res.stdout
+ logger.warning(f"Git pull --rebase failed: {stderr}")
+ # Abort any in-progress rebase to keep working tree stable
+ subprocess.run(
+ ["git", "-C", str(self.wiki_dir), "rebase", "--abort"],
+ capture_output=True,
+ check=False,
+ )
+ self.is_conflict = True
+ self.status = "conflict"
+ self.last_error = f"Rebase conflict: {stderr.strip()}"
+ return False
+ except Exception as e:
+ self.status = "error"
+ self.last_error = f"Pull error: {e}"
+ logger.error(self.last_error)
+ return False
+
+ def commit_and_push(self, commit_msg: str) -> bool:
+ """Stages data/, commits with descriptive message, rebases and pushes."""
+ if not (self.wiki_dir / ".git").is_dir():
+ return False
+
+ with self._lock:
+ try:
+ self._configure_git_client()
+ data_rel = "data"
+ subprocess.run(
+ ["git", "-C", str(self.wiki_dir), "add", data_rel],
+ capture_output=True,
+ check=False,
+ )
+ commit_res = subprocess.run(
+ ["git", "-C", str(self.wiki_dir), "commit", "-m", f"{commit_msg} [skip ci]"],
+ capture_output=True,
+ text=True,
+ check=False,
+ )
+ if commit_res.returncode != 0:
+ # Nothing to commit or already committed
+ logger.debug(f"Commit returncode {commit_res.returncode}: {commit_res.stdout.strip()}")
+ return True
+
+ # Pull rebase with autostash before push
+ auth_url = self.get_auth_repo_url()
+ subprocess.run(
+ ["git", "-C", str(self.wiki_dir), "remote", "set-url", "origin", auth_url],
+ capture_output=True,
+ check=False,
+ )
+
+ rebase_res = subprocess.run(
+ ["git", "-C", str(self.wiki_dir), "pull", "--rebase", "--autostash", "origin", GIT_BRANCH],
+ capture_output=True,
+ text=True,
+ check=False,
+ timeout=30,
+ )
+ if rebase_res.returncode != 0:
+ stderr = rebase_res.stderr or rebase_res.stdout
+ logger.warning(f"Rebase failed during commit_and_push: {stderr}")
+ subprocess.run(
+ ["git", "-C", str(self.wiki_dir), "rebase", "--abort"],
+ capture_output=True,
+ check=False,
+ )
+ self.is_conflict = True
+ self.status = "conflict"
+ self.last_error = f"Rebase conflict on push: {stderr.strip()}"
+ return False
+
+ # Push to remote
+ push_res = subprocess.run(
+ ["git", "-C", str(self.wiki_dir), "push", "origin", GIT_BRANCH],
+ capture_output=True,
+ text=True,
+ check=False,
+ timeout=30,
+ )
+ if push_res.returncode == 0:
+ self.status = "ok"
+ self.is_conflict = False
+ self.last_error = None
+ self.last_sync_time = datetime.datetime.now(datetime.timezone.utc).isoformat()
+ rev = subprocess.run(
+ ["git", "-C", str(self.wiki_dir), "rev-parse", "--short", "HEAD"],
+ capture_output=True,
+ text=True,
+ check=False,
+ )
+ if rev.returncode == 0:
+ self.last_commit_hash = rev.stdout.strip()
+ return True
+ else:
+ self.status = "error"
+ self.last_error = f"Push error: {push_res.stderr.strip()}"
+ logger.error(self.last_error)
+ return False
+ except Exception as e:
+ self.status = "error"
+ self.last_error = f"Commit/push error: {e}"
+ logger.error(self.last_error)
+ return False
+
+ async def start_periodic_sync(self):
+ """Starts background periodic git pull loop."""
+ if not AUTO_GIT_SYNC:
+ logger.info("Automatic git sync is disabled by configuration.")
+ return
+
+ self._stop_event.clear()
+
+ async def _loop():
+ logger.info(f"Starting periodic git sync every {GIT_SYNC_INTERVAL_SECONDS}s")
+ while not self._stop_event.is_set():
+ try:
+ await asyncio.to_thread(self.pull_rebase)
+ except Exception as ex:
+ logger.error(f"Error during periodic git pull: {ex}")
+ try:
+ await asyncio.wait_for(self._stop_event.wait(), timeout=GIT_SYNC_INTERVAL_SECONDS)
+ except asyncio.TimeoutError:
+ pass
+
+ self._bg_task = asyncio.create_task(_loop())
+
+ async def stop_periodic_sync(self):
+ """Stops background sync loop."""
+ self._stop_event.set()
+ if self._bg_task:
+ self._bg_task.cancel()
+ try:
+ await self._bg_task
+ except asyncio.CancelledError:
+ pass
+ self._bg_task = None
diff --git a/projects/duckdb-backend/app/main.py b/projects/duckdb-backend/app/main.py
new file mode 100644
index 0000000..56fc9e0
--- /dev/null
+++ b/projects/duckdb-backend/app/main.py
@@ -0,0 +1,67 @@
+from __future__ import annotations
+
+import logging
+from contextlib import asynccontextmanager
+from fastapi import FastAPI
+from fastapi.middleware.cors import CORSMiddleware
+
+from app.config import DATA_DIR, WIKI_DIR
+from app.engine import DuckDBEngine
+from app.git_sync import GitSyncManager
+from app.routes.dashboard import router as dashboard_router
+from app.routes.entities import router as entities_router
+from app.routes.events import router as events_router
+from app.routes.query import router as query_router
+from app.routes.system import router as system_router
+
+logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s")
+logger = logging.getLogger("duckdb_backend")
+
+
+@asynccontextmanager
+async def lifespan(app: FastAPI):
+ logger.info("Initializing DuckDB Backend...")
+ git_sync = GitSyncManager(
+ wiki_dir=WIKI_DIR,
+ on_data_pulled=lambda: app.state.engine.reload_all() if hasattr(app.state, "engine") else None,
+ )
+ git_sync.ensure_repo()
+ app.state.git_sync = git_sync
+
+ engine = DuckDBEngine(data_dir=DATA_DIR, git_sync_manager=git_sync)
+ app.state.engine = engine
+
+ await git_sync.start_periodic_sync()
+ logger.info("DuckDB Backend is ready to serve queries.")
+
+ yield
+
+ logger.info("Shutting down DuckDB Backend...")
+ await git_sync.stop_periodic_sync()
+
+
+app = FastAPI(
+ title="DuckDB Backend for Knowledge Wiki",
+ version="1.0.0",
+ description="HTTP-based analytical and transactional backend for personal logging data powered by DuckDB",
+ lifespan=lifespan,
+)
+
+app.add_middleware(
+ CORSMiddleware,
+ allow_origins=["*"],
+ allow_credentials=True,
+ allow_methods=["*"],
+ allow_headers=["*"],
+)
+
+# API v1 prefix
+v1_router = FastAPI()
+app.include_router(dashboard_router, prefix="/duckdb-api/v1")
+app.include_router(query_router, prefix="/duckdb-api/v1")
+app.include_router(events_router, prefix="/duckdb-api/v1")
+app.include_router(entities_router, prefix="/duckdb-api/v1")
+app.include_router(system_router, prefix="/duckdb-api/v1")
+
+# Direct root health probes for Kubernetes
+app.include_router(system_router)
diff --git a/projects/duckdb-backend/app/models.py b/projects/duckdb-backend/app/models.py
new file mode 100644
index 0000000..1c930f3
--- /dev/null
+++ b/projects/duckdb-backend/app/models.py
@@ -0,0 +1,55 @@
+from __future__ import annotations
+
+from typing import Any, Dict, List, Optional
+from pydantic import BaseModel, Field
+
+class QueryRequest(BaseModel):
+ query: str = Field(..., description="Read-only SQL query to execute in DuckDB")
+ params: Optional[List[Any]] = Field(default=None, description="Positional query parameters")
+
+class QueryResponse(BaseModel):
+ columns: List[str]
+ rows: List[Dict[str, Any]]
+ count: int
+ execution_time_ms: float
+
+class EventCreateRequest(BaseModel):
+ kind: str = Field(..., description="Event kind, e.g. sleep.session, nutrition.intake, diaper.change, feeding.bottle, growth.measurement")
+ subject: str = Field(..., description="Event subject, e.g. mila or moritz")
+ payload: Dict[str, Any] = Field(default_factory=dict, description="Event-specific payload")
+ actor: Optional[str] = Field(default="web-app", description="Actor who logged the event")
+ date: Optional[str] = Field(default=None, description="Explicit log date override YYYY-MM-DD")
+ start: Optional[str] = Field(default=None, description="ISO-8601 start timestamp")
+ end: Optional[str] = Field(default=None, description="ISO-8601 end timestamp (optional)")
+ since: Optional[str] = Field(default=None, description="Relative start duration like '15m', '1h'")
+ estimated: bool = Field(default=False, description="Whether intake is estimated")
+ meal: Optional[str] = Field(default=None, description="Meal category, e.g. fruehstueck, mittagessen, abendessen, snack")
+ note: Optional[str] = Field(default=None, description="Free-text note")
+ source_ref: Optional[str] = Field(default=None, description="Source reference")
+ allow_duplicate: bool = Field(default=False, description="Allow event within 15min of duplicate")
+
+class EventStopRequest(BaseModel):
+ kind: str = Field(default="sleep.session", description="Event kind to stop")
+ subject: str = Field(..., description="Event subject, e.g. mila")
+ end_time: Optional[str] = Field(default=None, description="ISO-8601 end timestamp")
+ since: Optional[str] = Field(default=None, description="Relative end duration like '5m'")
+ event_id: Optional[str] = Field(default=None, description="Specific event ID to close")
+ actor: Optional[str] = Field(default="web-app", description="Actor who stopped the event")
+
+class EntityPutRequest(BaseModel):
+ collection: str = Field(..., description="Target collection: foods, recipes, places, people")
+ record: Dict[str, Any] = Field(..., description="Entity record with 'id'")
+
+class GarminSyncRequest(BaseModel):
+ daily: List[Dict[str, Any]] = Field(default_factory=list, description="Garmin daily summary records")
+ sleep: List[Dict[str, Any]] = Field(default_factory=list, description="Garmin sleep records")
+ activities: List[Dict[str, Any]] = Field(default_factory=list, description="Garmin activity records")
+
+class StatusResponse(BaseModel):
+ status: str
+ git_conflict: bool
+ last_sync_time: Optional[str]
+ last_commit_hash: Optional[str]
+ last_error: Optional[str]
+ data_dir: str
+ table_counts: Dict[str, int]
diff --git a/projects/duckdb-backend/app/routes/__init__.py b/projects/duckdb-backend/app/routes/__init__.py
new file mode 100644
index 0000000..d0a5c98
--- /dev/null
+++ b/projects/duckdb-backend/app/routes/__init__.py
@@ -0,0 +1 @@
+# routes package
diff --git a/projects/duckdb-backend/app/routes/dashboard.py b/projects/duckdb-backend/app/routes/dashboard.py
new file mode 100644
index 0000000..d3e36e0
--- /dev/null
+++ b/projects/duckdb-backend/app/routes/dashboard.py
@@ -0,0 +1,68 @@
+from __future__ import annotations
+
+from typing import Any, Dict, Optional
+from fastapi import APIRouter, Depends, HTTPException, Query, Request
+
+from app.engine import DuckDBEngine
+
+router = APIRouter(tags=["Dashboards"])
+
+
+def get_engine(request: Request) -> DuckDBEngine:
+ return request.app.state.engine
+
+
+@router.get("/mila/dashboard")
+async def get_mila_dashboard(
+ as_of: Optional[str] = Query(default=None, description="Optional target date YYYY-MM-DD"),
+ engine: DuckDBEngine = Depends(get_engine),
+) -> Dict[str, Any]:
+ try:
+ return engine.get_mila_dashboard(as_of_date=as_of)
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=str(e))
+
+
+@router.get("/moritz/dashboard")
+async def get_moritz_dashboard(
+ as_of: Optional[str] = Query(default=None, description="Optional target date YYYY-MM-DD"),
+ engine: DuckDBEngine = Depends(get_engine),
+) -> Dict[str, Any]:
+ try:
+ return engine.get_moritz_dashboard(as_of_date=as_of)
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=str(e))
+
+
+@router.get("/mila/day/{date}")
+async def get_mila_day_detail(
+ date: str,
+ engine: DuckDBEngine = Depends(get_engine),
+) -> Dict[str, Any]:
+ try:
+ return engine.get_mila_day_detail(target_date=date)
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=str(e))
+
+
+@router.get("/moritz/day/{date}")
+async def get_moritz_day_detail(
+ date: str,
+ engine: DuckDBEngine = Depends(get_engine),
+) -> Dict[str, Any]:
+ try:
+ return engine.get_moritz_day_detail(target_date=date)
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=str(e))
+
+
+@router.get("/summary/{subject}")
+async def get_summary(
+ subject: str,
+ date: Optional[str] = Query(default=None, description="Target date YYYY-MM-DD"),
+ engine: DuckDBEngine = Depends(get_engine),
+) -> Dict[str, Any]:
+ try:
+ return engine.get_summary(subject=subject, date_str=date)
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=str(e))
diff --git a/projects/duckdb-backend/app/routes/entities.py b/projects/duckdb-backend/app/routes/entities.py
new file mode 100644
index 0000000..ef766a1
--- /dev/null
+++ b/projects/duckdb-backend/app/routes/entities.py
@@ -0,0 +1,51 @@
+from typing import Any, Dict, List, Optional
+from fastapi import APIRouter, Depends, HTTPException, Query, Request
+
+from app.engine import DuckDBEngine
+from app.models import EntityPutRequest
+
+router = APIRouter(tags=["Entities"])
+
+
+def get_engine(request: Request) -> DuckDBEngine:
+ return request.app.state.engine
+
+
+@router.post("/entities")
+async def put_entity(
+ req: EntityPutRequest,
+ engine: DuckDBEngine = Depends(get_engine),
+) -> Dict[str, Any]:
+ try:
+ return engine.put_entity(req)
+ except ValueError as ve:
+ raise HTTPException(status_code=400, detail=str(ve))
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=str(e))
+
+
+@router.get("/entities/{collection}")
+async def list_entities(
+ collection: str,
+ engine: DuckDBEngine = Depends(get_engine),
+) -> List[Dict[str, Any]]:
+ try:
+ return engine.list_entities(collection=collection)
+ except ValueError as ve:
+ raise HTTPException(status_code=400, detail=str(ve))
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=str(e))
+
+
+@router.get("/places/near")
+async def find_places_near(
+ lat: float = Query(..., description="Latitude"),
+ lon: float = Query(..., description="Longitude"),
+ radius_m: float = Query(default=1000.0, description="Radius in meters"),
+ category: Optional[str] = Query(default=None, description="Category filter"),
+ engine: DuckDBEngine = Depends(get_engine),
+) -> List[Dict[str, Any]]:
+ try:
+ return engine.find_places_near(lat=lat, lon=lon, radius_m=radius_m, category=category)
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=str(e))
diff --git a/projects/duckdb-backend/app/routes/events.py b/projects/duckdb-backend/app/routes/events.py
new file mode 100644
index 0000000..82627a2
--- /dev/null
+++ b/projects/duckdb-backend/app/routes/events.py
@@ -0,0 +1,65 @@
+from __future__ import annotations
+
+from typing import Any, Dict
+from fastapi import APIRouter, Depends, HTTPException, Request
+
+from app.engine import DuckDBEngine
+from app.models import EventCreateRequest, EventStopRequest, GarminSyncRequest
+
+router = APIRouter(tags=["Events"])
+
+
+def get_engine(request: Request) -> DuckDBEngine:
+ return request.app.state.engine
+
+
+@router.post("/events")
+async def add_event(
+ req: EventCreateRequest,
+ engine: DuckDBEngine = Depends(get_engine),
+) -> Dict[str, Any]:
+ try:
+ return engine.add_event(req)
+ except ValueError as ve:
+ raise HTTPException(status_code=400, detail=str(ve))
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=str(e))
+
+
+@router.post("/events/stop")
+async def stop_event(
+ req: EventStopRequest,
+ engine: DuckDBEngine = Depends(get_engine),
+) -> Dict[str, Any]:
+ try:
+ return engine.stop_event(req)
+ except ValueError as ve:
+ raise HTTPException(status_code=400, detail=str(ve))
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=str(e))
+
+
+@router.delete("/events/{event_id}")
+async def delete_event(
+ event_id: str,
+ engine: DuckDBEngine = Depends(get_engine),
+) -> Dict[str, Any]:
+ try:
+ return engine.delete_event(event_id)
+ except ValueError as ve:
+ raise HTTPException(status_code=404, detail=str(ve))
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=str(e))
+
+
+@router.post("/garmin/sync")
+async def sync_garmin(
+ req: GarminSyncRequest,
+ engine: DuckDBEngine = Depends(get_engine),
+) -> Dict[str, Any]:
+ try:
+ return engine.sync_garmin(req)
+ except ValueError as ve:
+ raise HTTPException(status_code=400, detail=str(ve))
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=str(e))
diff --git a/projects/duckdb-backend/app/routes/query.py b/projects/duckdb-backend/app/routes/query.py
new file mode 100644
index 0000000..ab53c77
--- /dev/null
+++ b/projects/duckdb-backend/app/routes/query.py
@@ -0,0 +1,35 @@
+from __future__ import annotations
+
+import time
+from fastapi import APIRouter, Depends, HTTPException, Request
+
+from app.engine import DuckDBEngine
+from app.models import QueryRequest, QueryResponse
+
+router = APIRouter(tags=["Generic Queries"])
+
+
+def get_engine(request: Request) -> DuckDBEngine:
+ return request.app.state.engine
+
+
+@router.post("/query", response_model=QueryResponse)
+async def execute_query(
+ req: QueryRequest,
+ engine: DuckDBEngine = Depends(get_engine),
+) -> QueryResponse:
+ t0 = time.perf_counter()
+ try:
+ rows = engine.query(req.query, req.params)
+ duration_ms = round((time.perf_counter() - t0) * 1000, 2)
+ cols = list(rows[0].keys()) if rows else []
+ return QueryResponse(
+ columns=cols,
+ rows=rows,
+ count=len(rows),
+ execution_time_ms=duration_ms,
+ )
+ except ValueError as ve:
+ raise HTTPException(status_code=400, detail=str(ve))
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=str(e))
diff --git a/projects/duckdb-backend/app/routes/system.py b/projects/duckdb-backend/app/routes/system.py
new file mode 100644
index 0000000..efb0f21
--- /dev/null
+++ b/projects/duckdb-backend/app/routes/system.py
@@ -0,0 +1,77 @@
+from __future__ import annotations
+
+from typing import Any, Dict
+from fastapi import APIRouter, Depends, HTTPException, Request
+
+from app.config import DATA_DIR
+from app.engine import DuckDBEngine
+from app.git_sync import GitSyncManager
+from app.models import StatusResponse
+
+router = APIRouter(tags=["System"])
+
+
+def get_engine(request: Request) -> DuckDBEngine:
+ return request.app.state.engine
+
+
+def get_git_sync(request: Request) -> Optional[GitSyncManager]:
+ return getattr(request.app.state, "git_sync", None)
+
+
+@router.get("/healthz")
+async def healthz(
+ request: Request,
+ engine: DuckDBEngine = Depends(get_engine),
+) -> Dict[str, Any]:
+ git_sync: Optional[GitSyncManager] = get_git_sync(request)
+ conflict = git_sync.is_conflict if git_sync else False
+ status_str = "conflict" if conflict else "ok"
+ return {
+ "status": status_str,
+ "conflict": conflict,
+ "engine_ready": True,
+ }
+
+
+@router.get("/status", response_model=StatusResponse)
+async def status(
+ request: Request,
+ engine: DuckDBEngine = Depends(get_engine),
+) -> StatusResponse:
+ git_sync: Optional[GitSyncManager] = get_git_sync(request)
+ counts = engine.get_table_counts()
+ return StatusResponse(
+ status=git_sync.status if git_sync else "ok",
+ git_conflict=git_sync.is_conflict if git_sync else False,
+ last_sync_time=git_sync.last_sync_time if git_sync else None,
+ last_commit_hash=git_sync.last_commit_hash if git_sync else None,
+ last_error=git_sync.last_error if git_sync else None,
+ data_dir=str(engine.data_dir),
+ table_counts=counts,
+ )
+
+
+@router.post("/git/sync")
+async def trigger_sync(
+ request: Request,
+ engine: DuckDBEngine = Depends(get_engine),
+) -> Dict[str, Any]:
+ git_sync: Optional[GitSyncManager] = get_git_sync(request)
+ if not git_sync:
+ raise HTTPException(status_code=400, detail="Git sync is not configured")
+
+ success = git_sync.pull_rebase()
+ if success:
+ engine.reload_all()
+ return {
+ "status": "ok",
+ "message": "Git pull --rebase completed and DuckDB reloaded.",
+ "commit": git_sync.last_commit_hash,
+ }
+ else:
+ return {
+ "status": git_sync.status,
+ "conflict": git_sync.is_conflict,
+ "error": git_sync.last_error,
+ }
diff --git a/projects/duckdb-backend/app/who_growth.py b/projects/duckdb-backend/app/who_growth.py
new file mode 100644
index 0000000..371e6d7
--- /dev/null
+++ b/projects/duckdb-backend/app/who_growth.py
@@ -0,0 +1,128 @@
+from __future__ import annotations
+
+import math
+from typing import Any, Dict, List, Optional, Tuple
+
+WHO_GIRLS_WEIGHT = [
+ {"month": 0, "L": 0.3809, "M": 3.2322, "S": 0.14171, "p3": 2.4, "p50": 3.2, "p97": 4.2},
+ {"month": 1, "L": 0.1714, "M": 4.1873, "S": 0.13724, "p3": 3.2, "p50": 4.2, "p97": 5.4},
+ {"month": 2, "L": 0.0962, "M": 5.1282, "S": 0.13000, "p3": 4.0, "p50": 5.1, "p97": 6.5},
+ {"month": 3, "L": 0.0402, "M": 5.8458, "S": 0.12619, "p3": 4.6, "p50": 5.8, "p97": 7.4},
+ {"month": 4, "L": -0.0050, "M": 6.4237, "S": 0.12402, "p3": 5.1, "p50": 6.4, "p97": 8.1},
+ {"month": 5, "L": -0.0430, "M": 6.8985, "S": 0.12274, "p3": 5.5, "p50": 6.9, "p97": 8.7},
+ {"month": 6, "L": -0.0756, "M": 7.2970, "S": 0.12204, "p3": 5.8, "p50": 7.3, "p97": 9.2},
+ {"month": 7, "L": -0.1039, "M": 7.6422, "S": 0.12178, "p3": 6.1, "p50": 7.6, "p97": 9.6},
+ {"month": 8, "L": -0.1288, "M": 7.9487, "S": 0.12181, "p3": 6.3, "p50": 7.9, "p97": 10.0},
+ {"month": 9, "L": -0.1507, "M": 8.2254, "S": 0.12199, "p3": 6.6, "p50": 8.2, "p97": 10.4},
+ {"month": 10, "L": -0.1700, "M": 8.4800, "S": 0.12223, "p3": 6.8, "p50": 8.5, "p97": 10.7},
+ {"month": 11, "L": -0.1872, "M": 8.7192, "S": 0.12247, "p3": 7.0, "p50": 8.7, "p97": 11.0},
+ {"month": 12, "L": -0.2024, "M": 8.9481, "S": 0.12268, "p3": 7.1, "p50": 8.9, "p97": 11.3},
+ {"month": 13, "L": -0.2158, "M": 9.1699, "S": 0.12283, "p3": 7.3, "p50": 9.2, "p97": 11.6},
+ {"month": 14, "L": -0.2278, "M": 9.3870, "S": 0.12294, "p3": 7.5, "p50": 9.4, "p97": 11.9},
+ {"month": 15, "L": -0.2384, "M": 9.6008, "S": 0.12299, "p3": 7.7, "p50": 9.6, "p97": 12.2},
+ {"month": 16, "L": -0.2478, "M": 9.8124, "S": 0.12303, "p3": 7.8, "p50": 9.8, "p97": 12.5},
+ {"month": 17, "L": -0.2562, "M": 10.0226, "S": 0.12306, "p3": 8.0, "p50": 10.0, "p97": 12.7},
+ {"month": 18, "L": -0.2637, "M": 10.2315, "S": 0.12309, "p3": 8.2, "p50": 10.2, "p97": 13.0},
+ {"month": 19, "L": -0.2703, "M": 10.4393, "S": 0.12315, "p3": 8.3, "p50": 10.4, "p97": 13.3},
+ {"month": 20, "L": -0.2762, "M": 10.6464, "S": 0.12323, "p3": 8.5, "p50": 10.6, "p97": 13.5},
+ {"month": 21, "L": -0.2815, "M": 10.8534, "S": 0.12335, "p3": 8.7, "p50": 10.9, "p97": 13.8},
+ {"month": 22, "L": -0.2862, "M": 11.0608, "S": 0.12350, "p3": 8.8, "p50": 11.1, "p97": 14.1},
+ {"month": 23, "L": -0.2903, "M": 11.2688, "S": 0.12369, "p3": 9.0, "p50": 11.3, "p97": 14.3},
+ {"month": 24, "L": -0.2941, "M": 11.4775, "S": 0.12390, "p3": 9.2, "p50": 11.5, "p97": 14.6},
+]
+
+WHO_GIRLS_HEIGHT = [
+ {"month": 0, "L": 1.0, "M": 49.1477, "S": 0.03790, "p3": 45.6, "p50": 49.1, "p97": 52.7},
+ {"month": 1, "L": 1.0, "M": 53.6872, "S": 0.03640, "p3": 50.0, "p50": 53.7, "p97": 57.4},
+ {"month": 2, "L": 1.0, "M": 57.0673, "S": 0.03568, "p3": 53.2, "p50": 57.1, "p97": 60.9},
+ {"month": 3, "L": 1.0, "M": 59.8029, "S": 0.03520, "p3": 55.8, "p50": 59.8, "p97": 63.8},
+ {"month": 4, "L": 1.0, "M": 62.0899, "S": 0.03486, "p3": 58.0, "p50": 62.1, "p97": 66.2},
+ {"month": 5, "L": 1.0, "M": 64.0301, "S": 0.03463, "p3": 59.9, "p50": 64.0, "p97": 68.2},
+ {"month": 6, "L": 1.0, "M": 65.7311, "S": 0.03448, "p3": 61.5, "p50": 65.7, "p97": 70.0},
+ {"month": 7, "L": 1.0, "M": 67.2873, "S": 0.03441, "p3": 62.9, "p50": 67.3, "p97": 71.6},
+ {"month": 8, "L": 1.0, "M": 68.7498, "S": 0.03440, "p3": 64.3, "p50": 68.7, "p97": 73.2},
+ {"month": 9, "L": 1.0, "M": 70.1435, "S": 0.03444, "p3": 65.6, "p50": 70.1, "p97": 74.7},
+ {"month": 10, "L": 1.0, "M": 71.4818, "S": 0.03452, "p3": 66.8, "p50": 71.5, "p97": 76.1},
+ {"month": 11, "L": 1.0, "M": 72.7710, "S": 0.03464, "p3": 68.0, "p50": 72.8, "p97": 77.5},
+ {"month": 12, "L": 1.0, "M": 74.0150, "S": 0.03479, "p3": 69.2, "p50": 74.0, "p97": 78.9},
+ {"month": 13, "L": 1.0, "M": 75.2176, "S": 0.03496, "p3": 70.3, "p50": 75.2, "p97": 80.2},
+ {"month": 14, "L": 1.0, "M": 76.3817, "S": 0.03514, "p3": 71.3, "p50": 76.4, "p97": 81.4},
+ {"month": 15, "L": 1.0, "M": 77.5099, "S": 0.03534, "p3": 72.4, "p50": 77.5, "p97": 82.7},
+ {"month": 16, "L": 1.0, "M": 78.6055, "S": 0.03555, "p3": 73.3, "p50": 78.6, "p97": 83.9},
+ {"month": 17, "L": 1.0, "M": 79.6710, "S": 0.03576, "p3": 74.3, "p50": 79.7, "p97": 85.0},
+ {"month": 18, "L": 1.0, "M": 80.7079, "S": 0.03598, "p3": 75.2, "p50": 80.7, "p97": 86.2},
+ {"month": 19, "L": 1.0, "M": 81.7182, "S": 0.03620, "p3": 76.2, "p50": 81.7, "p97": 87.3},
+ {"month": 20, "L": 1.0, "M": 82.7036, "S": 0.03643, "p3": 77.0, "p50": 82.7, "p97": 88.4},
+ {"month": 21, "L": 1.0, "M": 83.6654, "S": 0.03666, "p3": 77.9, "p50": 83.7, "p97": 89.4},
+ {"month": 22, "L": 1.0, "M": 84.6040, "S": 0.03688, "p3": 78.7, "p50": 84.6, "p97": 90.5},
+ {"month": 23, "L": 1.0, "M": 85.5202, "S": 0.03711, "p3": 79.6, "p50": 85.5, "p97": 91.5},
+ {"month": 24, "L": 1.0, "M": 86.4153, "S": 0.03734, "p3": 80.3, "p50": 86.4, "p97": 92.5},
+]
+
+def _interpolate_lms(table: List[Dict[str, float]], age_months: float) -> Tuple[float, float, float, float, float, float]:
+ m_clamped = max(0.0, min(float(table[-1]["month"]), float(age_months)))
+ m_low = int(math.floor(m_clamped))
+ m_high = int(math.ceil(m_clamped))
+
+ r_low = table[m_low]
+ if m_low == m_high:
+ return r_low["L"], r_low["M"], r_low["S"], r_low["p3"], r_low["p50"], r_low["p97"]
+
+ r_high = table[m_high]
+ frac = m_clamped - m_low
+
+ L = r_low["L"] + frac * (r_high["L"] - r_low["L"])
+ M = r_low["M"] + frac * (r_high["M"] - r_low["M"])
+ S = r_low["S"] + frac * (r_high["S"] - r_low["S"])
+ p3 = r_low["p3"] + frac * (r_high["p3"] - r_low["p3"])
+ p50 = r_low["p50"] + frac * (r_high["p50"] - r_low["p50"])
+ p97 = r_low["p97"] + frac * (r_high["p97"] - r_low["p97"])
+
+ return L, M, S, p3, p50, p97
+
+def calculate_percentile(metric: str, value: float, age_months: float) -> Dict[str, Any]:
+ table = WHO_GIRLS_WEIGHT if metric == "weight" else WHO_GIRLS_HEIGHT
+ L, M, S, p3, p50, p97 = _interpolate_lms(table, age_months)
+
+ if value <= 0 or M <= 0 or S <= 0:
+ return {
+ "value": value,
+ "age_months": round(age_months, 2),
+ "z_score": 0.0,
+ "percentile": 50.0,
+ "percentile_str": "P50",
+ "p3": round(p3, 2),
+ "p50": round(p50, 2),
+ "p97": round(p97, 2),
+ "in_normal_range": True,
+ "status": "normal",
+ }
+
+ if abs(L) > 1e-6:
+ z = ((value / M) ** L - 1.0) / (L * S)
+ else:
+ z = math.log(value / M) / S
+
+ p = 0.5 * (1.0 + math.erf(z / math.sqrt(2.0))) * 100.0
+ p_clamped = max(0.1, min(99.9, p))
+
+ in_normal = 3.0 <= p_clamped <= 97.0
+ if p_clamped < 3.0:
+ status = "unter P3"
+ elif p_clamped > 97.0:
+ status = "über P97"
+ else:
+ status = "im WHO-Normbereich"
+
+ return {
+ "value": value,
+ "age_months": round(age_months, 2),
+ "z_score": round(z, 2),
+ "percentile": round(p_clamped, 1),
+ "percentile_str": f"P{int(round(p_clamped))}",
+ "p3": round(p3, 2),
+ "p50": round(p50, 2),
+ "p97": round(p97, 2),
+ "in_normal_range": in_normal,
+ "status": status,
+ }
diff --git a/projects/duckdb-backend/pyproject.toml b/projects/duckdb-backend/pyproject.toml
new file mode 100644
index 0000000..b5263fd
--- /dev/null
+++ b/projects/duckdb-backend/pyproject.toml
@@ -0,0 +1,28 @@
+[project]
+name = "duckdb-backend"
+version = "1.0.0"
+description = "FastAPI HTTP DuckDB Backend for Knowledge Wiki Personal Data"
+readme = "README.md"
+requires-python = ">=3.12"
+dependencies = [
+ "fastapi>=0.115.0",
+ "uvicorn[standard]>=0.30.0",
+ "duckdb>=1.1.0",
+ "python-ulid>=3.0.0",
+ "pytz>=2024.1",
+ "pydantic>=2.8.0",
+ "httpx>=0.27.0",
+]
+
+[project.optional-dependencies]
+dev = [
+ "pytest>=8.0.0",
+ "pytest-asyncio>=0.24.0",
+]
+
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[tool.hatch.build.targets.wheel]
+packages = ["app"]
diff --git a/projects/duckdb-backend/tests/conftest.py b/projects/duckdb-backend/tests/conftest.py
new file mode 100644
index 0000000..5e76595
--- /dev/null
+++ b/projects/duckdb-backend/tests/conftest.py
@@ -0,0 +1,35 @@
+import os
+import shutil
+import tempfile
+from pathlib import Path
+import pytest
+from fastapi.testclient import TestClient
+
+from app.config import DATA_DIR
+from app.engine import DuckDBEngine
+from app.git_sync import GitSyncManager
+from app.main import app
+
+
+@pytest.fixture(scope="session")
+def test_data_dir():
+ # Use real wiki data dir if exists, else temporary copy
+ if DATA_DIR.is_dir():
+ yield DATA_DIR
+ else:
+ temp_dir = Path(tempfile.mkdtemp())
+ yield temp_dir
+ shutil.rmtree(temp_dir, ignore_errors=True)
+
+
+@pytest.fixture
+def engine(test_data_dir):
+ return DuckDBEngine(data_dir=test_data_dir)
+
+
+@pytest.fixture
+def client(engine):
+ app.state.engine = engine
+ app.state.git_sync = GitSyncManager(wiki_dir=engine.data_dir.parent)
+ with TestClient(app) as c:
+ yield c
diff --git a/projects/duckdb-backend/tests/test_api_curated.py b/projects/duckdb-backend/tests/test_api_curated.py
new file mode 100644
index 0000000..bb9f38b
--- /dev/null
+++ b/projects/duckdb-backend/tests/test_api_curated.py
@@ -0,0 +1,179 @@
+import pytest
+from fastapi.testclient import TestClient
+
+
+def test_mila_dashboard_endpoint(client: TestClient):
+ resp = client.get("/duckdb-api/v1/mila/dashboard")
+ assert resp.status_code == 200
+ data = resp.json()
+ assert data["status"] == "ok"
+ assert data["metadata"]["subject"] == "mila"
+ assert "live_status" in data
+ assert "today" in data
+ assert "growth" in data
+
+
+def test_moritz_dashboard_endpoint(client: TestClient):
+ resp = client.get("/duckdb-api/v1/moritz/dashboard")
+ assert resp.status_code == 200
+ data = resp.json()
+ assert data["status"] == "ok"
+ assert data["metadata"]["subject"] == "moritz"
+ assert "today" in data
+ assert "yesterday" in data
+ assert "seven_days" in data
+ assert "thirty_days" in data
+
+
+def test_healthz_and_status_endpoints(client: TestClient):
+ resp_h = client.get("/healthz")
+ assert resp_h.status_code == 200
+ assert resp_h.json()["engine_ready"] is True
+
+ resp_s = client.get("/duckdb-api/v1/status")
+ assert resp_s.status_code == 200
+ data_s = resp_s.json()
+ assert "status" in data_s
+ assert "table_counts" in data_s
+ assert data_s["table_counts"]["sleep"] > 0
+
+
+def test_mila_history_includes_diaper_only_days(client: TestClient):
+ # Log a diaper change for a specific day without sleep
+ client.post("/duckdb-api/v1/events", json={
+ "kind": "diaper.change",
+ "subject": "mila",
+ "start": "2026-10-12T12:00:00+02:00",
+ "payload": {"pee": True, "poo": True},
+ })
+ resp = client.get("/duckdb-api/v1/mila/dashboard")
+ assert resp.status_code == 200
+ data = resp.json()
+ history = data["history"]
+ dates = [h["date"] for h in history]
+ assert "2026-10-12" in dates
+
+
+def test_moritz_seven_days_includes_garmin_days(client: TestClient):
+ resp = client.get("/duckdb-api/v1/moritz/dashboard")
+ assert resp.status_code == 200
+ data = resp.json()
+ days = data["seven_days"]["days"]
+ assert len(days) > 0
+ # Every returned day has a valid date and steps or expenditure
+ for d in days:
+ assert d["date"] is not None
+ assert "expenditure_kcal" in d
+
+
+def test_mila_dashboard_parent_cockpit_and_norms(client: TestClient):
+ resp = client.get("/duckdb-api/v1/mila/dashboard")
+ assert resp.status_code == 200
+ data = resp.json()
+
+ # Parent cockpit live fields
+ live = data["live_status"]
+ assert "is_sleeping" in live
+ assert "active_sleep_duration_str" in live
+ assert "wake_window_minutes" in live
+ assert "last_feed_since_str" in live
+ assert "last_diaper_since_str" in live
+ assert "last_stool_str" in live
+
+ # Science age norms
+ assert "age_norms" in data
+ norms = data["age_norms"]
+ assert norms["age_months"] >= 6.0
+ assert norms["wake_window_min_minutes"] == 135
+ assert norms["wake_window_max_minutes"] == 180
+ assert norms["feed_interval_min_minutes"] == 180
+
+ # Ensure IDs on items
+ today = data["today"]
+ for f in today["feedings"]:
+ assert "id" in f
+ for d in today["diapers"]:
+ assert "id" in d
+
+
+def test_day_detail_and_summary_endpoints(client: TestClient):
+ # Test mila day detail
+ resp_mila = client.get("/duckdb-api/v1/mila/day/2026-10-12")
+ assert resp_mila.status_code == 200
+ assert resp_mila.json()["metadata"]["today_date"] == "2026-10-12"
+
+ # Test moritz day detail
+ resp_moritz = client.get("/duckdb-api/v1/moritz/day/2026-10-12")
+ assert resp_moritz.status_code == 200
+ assert "today" in resp_moritz.json()
+
+ # Test summary
+ resp_sum = client.get("/duckdb-api/v1/summary/mila?date=2026-10-12")
+ assert resp_sum.status_code == 200
+ sum_data = resp_sum.json()
+ assert sum_data["subject"] == "mila"
+ assert sum_data["date"] == "2026-10-12"
+ assert "nutrition" in sum_data
+
+
+def test_entities_and_places_endpoints(client: TestClient):
+ # Put and list entity
+ client.post("/duckdb-api/v1/entities", json={
+ "collection": "places",
+ "record": {
+ "id": "test-biergarten",
+ "name": "Test Biergarten",
+ "category": "biergarten",
+ "lat": 49.02,
+ "lon": 12.09,
+ }
+ })
+
+ resp_places = client.get("/duckdb-api/v1/entities/places")
+ assert resp_places.status_code == 200
+ places = resp_places.json()
+ assert any(p["id"] == "test-biergarten" for p in places)
+
+ # Places near search
+ resp_near = client.get("/duckdb-api/v1/places/near?lat=49.02&lon=12.09&radius_m=500")
+ assert resp_near.status_code == 200
+ near = resp_near.json()
+ assert len(near) > 0
+ assert near[0]["id"] == "test-biergarten"
+
+
+def test_garmin_sync_and_duplicate_guard(client: TestClient):
+ # Duplicate guard test
+ post_data = {
+ "kind": "feeding.bottle",
+ "subject": "mila",
+ "start": "2026-10-12T10:00:00+02:00",
+ "payload": {"consumed_ml": 180, "milk_type": "pre"},
+ }
+ r1 = client.post("/duckdb-api/v1/events", json=post_data)
+ assert r1.status_code == 200
+ assert r1.json()["status"] == "success"
+ assert "feedback" in r1.json()
+
+ # Second post within 15min should be duplicate_skipped
+ r2 = client.post("/duckdb-api/v1/events", json=post_data)
+ assert r2.status_code == 200
+ assert r2.json()["status"] == "duplicate_skipped"
+
+ # Garmin sync test
+ garmin_payload = {
+ "daily": [{
+ "calendarDate": "2026-10-15",
+ "totalKilocalories": 2400,
+ "activeKilocalories": 600,
+ "bmrKilocalories": 1800,
+ "totalSteps": 11000,
+ "restingHeartRate": 52,
+ }],
+ "sleep": [],
+ "activities": [],
+ }
+ r_sync = client.post("/duckdb-api/v1/garmin/sync", json=garmin_payload)
+ assert r_sync.status_code == 200
+ assert r_sync.json()["status"] == "success"
+ assert r_sync.json()["synced_daily"] == 1
diff --git a/projects/duckdb-backend/tests/test_api_events.py b/projects/duckdb-backend/tests/test_api_events.py
new file mode 100644
index 0000000..1230215
--- /dev/null
+++ b/projects/duckdb-backend/tests/test_api_events.py
@@ -0,0 +1,129 @@
+import tempfile
+import pytest
+from pathlib import Path
+from fastapi.testclient import TestClient
+
+from app.engine import DuckDBEngine
+from app.models import EventCreateRequest, EventStopRequest
+
+
+def test_add_and_stop_sleep_event(tmp_path: Path):
+ # Use isolated temp directory for write tests so we do not pollute wiki git repo
+ engine = DuckDBEngine(data_dir=tmp_path)
+
+ # 1. Add active sleep event (end=None)
+ add_req = EventCreateRequest(
+ kind="sleep.session",
+ subject="mila",
+ payload={"type": "nap", "location": "kinderwagen"},
+ actor="test-runner",
+ note="Mittagsschlaf Test",
+ )
+ res_add = engine.add_event(add_req)
+ assert res_add["status"] == "success"
+ event_id = res_add["id"]
+ assert event_id is not None
+
+ # Check that it appears in open sleep
+ open_rows = engine.query(f"SELECT * FROM sleep WHERE id = '{event_id}' AND (isOpen = true OR endIso IS NULL)")
+ assert len(open_rows) == 1
+
+ # 2. Stop the active sleep event
+ stop_req = EventStopRequest(
+ kind="sleep.session",
+ subject="mila",
+ event_id=event_id,
+ actor="test-runner",
+ )
+ res_stop = engine.stop_event(stop_req)
+ assert res_stop["status"] == "success"
+ assert res_stop["id"] == event_id
+
+ # Check that it is now closed
+ closed_rows = engine.query(f"SELECT isOpen, endIso FROM sleep WHERE id = '{event_id}'")
+ assert len(closed_rows) == 1
+ assert closed_rows[0]["isOpen"] is False
+ assert closed_rows[0]["endIso"] is not None
+
+
+def test_add_nutrition_and_diaper_event(tmp_path: Path):
+ engine = DuckDBEngine(data_dir=tmp_path)
+
+ # Add nutrition event
+ nut_req = EventCreateRequest(
+ kind="nutrition.intake",
+ subject="moritz",
+ payload={"item": "Apfel", "qty": 150.0, "kcal": 78.0, "protein_g": 0.5},
+ meal="snack",
+ actor="test-runner",
+ )
+ res_nut = engine.add_event(nut_req)
+ assert res_nut["status"] == "success"
+ nut_id = res_nut["id"]
+
+ nut_rows = engine.query(f"SELECT * FROM nutrition WHERE id = '{nut_id}'")
+ assert len(nut_rows) == 1
+
+ # Add diaper event
+ diaper_req = EventCreateRequest(
+ kind="diaper.change",
+ subject="mila",
+ payload={"pee": True, "poo": False},
+ actor="test-runner",
+ )
+ res_diaper = engine.add_event(diaper_req)
+ assert res_diaper["status"] == "success"
+ diaper_id = res_diaper["id"]
+
+ diaper_rows = engine.query(f"SELECT * FROM mila WHERE id = '{diaper_id}'")
+ assert len(diaper_rows) == 1
+
+
+def test_duplicate_open_sleep_rejected(tmp_path: Path):
+ engine = DuckDBEngine(data_dir=tmp_path)
+
+ # 1. Start first open sleep session
+ req1 = EventCreateRequest(
+ kind="sleep.session",
+ subject="mila",
+ payload={"type": "nap", "location": "bett"},
+ actor="parent",
+ )
+ res1 = engine.add_event(req1)
+ assert res1["status"] == "success"
+
+ # 2. Attempt to start second open sleep session without stopping the first
+ req2 = EventCreateRequest(
+ kind="sleep.session",
+ subject="mila",
+ payload={"type": "nap", "location": "kinderwagen"},
+ actor="parent",
+ )
+ with pytest.raises(ValueError, match="already active for subject 'mila'"):
+ engine.add_event(req2)
+
+
+def test_duplicate_open_sleep_api_endpoint(client: TestClient):
+ # 1. Start first session via API
+ res1 = client.post("/duckdb-api/v1/events", json={
+ "kind": "sleep.session",
+ "subject": "test_baby",
+ "payload": {"type": "nap"},
+ })
+ assert res1.status_code == 200
+
+ # 2. Start second session via API -> must return 400 Bad Request
+ res2 = client.post("/duckdb-api/v1/events", json={
+ "kind": "sleep.session",
+ "subject": "test_baby",
+ "payload": {"type": "nap"},
+ })
+ assert res2.status_code == 400
+ assert "already active for subject 'test_baby'" in res2.json()["detail"]
+
+ # 3. Clean up by stopping
+ stop_res = client.post("/duckdb-api/v1/events/stop", json={
+ "kind": "sleep.session",
+ "subject": "test_baby",
+ })
+ assert stop_res.status_code == 200
diff --git a/projects/duckdb-backend/tests/test_api_query.py b/projects/duckdb-backend/tests/test_api_query.py
new file mode 100644
index 0000000..276a4cd
--- /dev/null
+++ b/projects/duckdb-backend/tests/test_api_query.py
@@ -0,0 +1,28 @@
+import pytest
+from fastapi.testclient import TestClient
+
+
+def test_generic_query_select(client: TestClient):
+ payload = {
+ "query": "SELECT count(*) AS cnt FROM v_sleep WHERE subject = 'mila'"
+ }
+ resp = client.post("/duckdb-api/v1/query", json=payload)
+ assert resp.status_code == 200
+ data = resp.json()
+ assert "rows" in data
+ assert len(data["rows"]) == 1
+ assert data["rows"][0]["cnt"] > 0
+ assert data["execution_time_ms"] < 50.0 # verifies sub-50ms execution
+
+
+def test_generic_query_disallowed_drop(client: TestClient):
+ payload = {"query": "DROP TABLE sleep;"}
+ resp = client.post("/duckdb-api/v1/query", json=payload)
+ assert resp.status_code == 400
+ assert "not permitted" in resp.json()["detail"]
+
+
+def test_generic_query_disallowed_insert(client: TestClient):
+ payload = {"query": "INSERT INTO sleep VALUES ('x', 'y')"}
+ resp = client.post("/duckdb-api/v1/query", json=payload)
+ assert resp.status_code == 400
diff --git a/projects/duckdb-backend/tests/test_engine.py b/projects/duckdb-backend/tests/test_engine.py
new file mode 100644
index 0000000..811b006
--- /dev/null
+++ b/projects/duckdb-backend/tests/test_engine.py
@@ -0,0 +1,36 @@
+import pytest
+from app.engine import DuckDBEngine
+
+
+def test_engine_init_and_counts(engine: DuckDBEngine):
+ counts = engine.get_table_counts()
+ assert "sleep" in counts
+ assert "nutrition" in counts
+ assert "mila" in counts
+ assert "garmin_daily" in counts
+ assert counts["sleep"] > 0
+ assert counts["nutrition"] > 0
+
+
+def test_mila_dashboard_payload(engine: DuckDBEngine):
+ data = engine.get_mila_dashboard()
+ assert data["status"] == "ok"
+ assert data["metadata"]["subject"] == "mila"
+ assert "live_status" in data
+ assert "today" in data
+ assert "averages" in data
+ assert "history" in data
+ assert "growth" in data
+ assert len(data["history"]) > 0
+
+
+def test_moritz_dashboard_payload(engine: DuckDBEngine):
+ data = engine.get_moritz_dashboard()
+ assert data["status"] == "ok"
+ assert data["metadata"]["subject"] == "moritz"
+ assert "today" in data
+ assert "yesterday" in data
+ assert "garmin_recovery" in data
+ assert "seven_days" in data
+ assert "thirty_days" in data
+ assert len(data["thirty_days"]["days"]) > 0
diff --git a/projects/duckdb-backend/tests/test_git_sync.py b/projects/duckdb-backend/tests/test_git_sync.py
new file mode 100644
index 0000000..1103746
--- /dev/null
+++ b/projects/duckdb-backend/tests/test_git_sync.py
@@ -0,0 +1,68 @@
+import subprocess
+from pathlib import Path
+from app.git_sync import GitSyncManager
+
+
+def test_git_sync_conflict_handling(tmp_path: Path):
+ repo_dir = tmp_path / "fake_repo"
+ repo_dir.mkdir()
+ subprocess.run(["git", "init", "-b", "main", str(repo_dir)], check=True)
+ subprocess.run(["git", "-C", str(repo_dir), "config", "user.email", "test@test.local"], check=True)
+ subprocess.run(["git", "-C", str(repo_dir), "config", "user.name", "Test User"], check=True)
+
+ test_file = repo_dir / "test.txt"
+ test_file.write_text("initial")
+ subprocess.run(["git", "-C", str(repo_dir), "add", "."], check=True)
+ subprocess.run(["git", "-C", str(repo_dir), "commit", "-m", "initial commit"], check=True)
+
+ mgr = GitSyncManager(wiki_dir=repo_dir)
+ assert mgr.ensure_repo() is True
+ assert mgr.status == "ok"
+ assert mgr.is_conflict is False
+
+
+def test_git_url_encoding(monkeypatch):
+ monkeypatch.setattr("app.git_sync.GIT_USERNAME", "moritz@graf")
+ monkeypatch.setattr("app.git_sync.GIT_PASSWORD", "p@ss:w/ord%123")
+ monkeypatch.setattr("app.git_sync.GIT_REPO_URL", "https://git.moritzgraf.de/moritz/knowledge-wiki.git")
+
+ mgr = GitSyncManager()
+ auth_url = mgr.get_auth_repo_url()
+ assert auth_url == "https://moritz%40graf:p%40ss%3Aw%2Ford%25123@git.moritzgraf.de/moritz/knowledge-wiki.git"
+
+
+def test_ensure_repo_handles_non_empty_dir(tmp_path: Path):
+ # Simulate a PVC containing lost+found before repo initialization
+ wiki_pvc = tmp_path / "wiki_pvc"
+ wiki_pvc.mkdir()
+ (wiki_pvc / "lost+found").mkdir()
+
+ # Create upstream origin to fetch from
+ origin = tmp_path / "origin.git"
+ subprocess.run(["git", "init", "--bare", str(origin)], check=True)
+
+ # Push initial commit to bare origin
+ work = tmp_path / "work"
+ work.mkdir()
+ subprocess.run(["git", "init", "-b", "main", str(work)], check=True)
+ subprocess.run(["git", "-C", str(work), "config", "user.name", "Admin"], check=True)
+ subprocess.run(["git", "-C", str(work), "config", "user.email", "admin@test.local"], check=True)
+ (work / "README.md").write_text("# Knowledge Wiki")
+ subprocess.run(["git", "-C", str(work), "add", "."], check=True)
+ subprocess.run(["git", "-C", str(work), "commit", "-m", "init"], check=True)
+ subprocess.run(["git", "-C", str(work), "remote", "add", "origin", str(origin)], check=True)
+ subprocess.run(["git", "-C", str(work), "push", "origin", "main"], check=True)
+
+ # Now verify GitSyncManager initializes cleanly into non-empty directory
+ mgr = GitSyncManager(wiki_dir=wiki_pvc)
+ # Point URL to file repo
+ import app.git_sync
+ app.git_sync.GIT_REPO_URL = str(origin)
+ app.git_sync.GIT_USERNAME = ""
+ app.git_sync.GIT_PASSWORD = ""
+
+ success = mgr.ensure_repo()
+ assert success is True
+ assert (wiki_pvc / ".git").is_dir()
+ assert (wiki_pvc / "README.md").is_file()
+ assert (wiki_pvc / "lost+found").is_dir()
diff --git a/projects/duckdb-backend/uv.lock b/projects/duckdb-backend/uv.lock
new file mode 100644
index 0000000..9ef91ff
--- /dev/null
+++ b/projects/duckdb-backend/uv.lock
@@ -0,0 +1,813 @@
+version = 1
+revision = 3
+requires-python = ">=3.12"
+
+[[package]]
+name = "annotated-doc"
+version = "0.0.5"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/5a/8e/38aa427ed5402449e226975b649c5dc73ccadfefeb95e6aecb8f8ea4b6b6/annotated_doc-0.0.5.tar.gz", hash = "sha256:c7e58ce09192557605d8bbd92836d7e1d520ac9580096042c0bfd197efacf1bb", size = 10758, upload-time = "2026-07-28T13:50:58.129Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3e/30/e900b21425a860e195f32e37657aa1f7c7f2b1bfb26f03ca209b90933c06/annotated_doc-0.0.5-py3-none-any.whl", hash = "sha256:117bac03a25ede5df5440e855b32d556049ca169ead221505badf432fed4b101", size = 5302, upload-time = "2026-07-28T13:50:57.239Z" },
+]
+
+[[package]]
+name = "annotated-types"
+version = "0.8.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/5f/56/a8120250d128bed162cd73c76d45f6ef9991f3e068f62a8ee060afa3104a/annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7", size = 15893, upload-time = "2026-07-23T20:16:13.995Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/99/91/8acff4f5e50511b911bbccb72b8628a49c68ce14148cd9f6431094859a90/annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0", size = 13427, upload-time = "2026-07-23T20:16:12.938Z" },
+]
+
+[[package]]
+name = "anyio"
+version = "4.15.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "idna" },
+ { name = "typing-extensions", marker = "python_full_version < '3.15'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a9/d2/f4d173e22df740bc37b1db102b386ba719b66e95b0f0d751f556b387e6d2/anyio-4.15.1.tar.gz", hash = "sha256:9f28306018cbd6d329e64a36d58256edff76dd996fe423bc957326e578b82a94", size = 276966, upload-time = "2026-09-05T10:42:39.44Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/12/b8/4bd346e22b28902df4d651910f5242c28d84e4a5c2435ca5c3f797ed7e2e/anyio-4.15.1-py3-none-any.whl", hash = "sha256:6152fdbbf9a77fdec97731721bebf7c4c44f7c29b424b0065826173efc7ed101", size = 132079, upload-time = "2026-09-05T10:42:37.923Z" },
+]
+
+[[package]]
+name = "certifi"
+version = "2026.7.22"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" },
+]
+
+[[package]]
+name = "click"
+version = "8.5.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/c7/0e/7fa0ef50764b67090eca4114772a2abf8b6148198475e54c660b97caeee6/click-8.5.0.tar.gz", hash = "sha256:ba0d2089de75ea0310e2dde03160e6ca10009947fb95a182f9b54021bb272e34", size = 382235, upload-time = "2026-08-26T13:33:14.56Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/58/50/6c0d534c5f134586a8e1ba4e330569e32f057e33372ae556463212fb4cd3/click-8.5.0-py3-none-any.whl", hash = "sha256:255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360", size = 125251, upload-time = "2026-08-26T13:33:12.928Z" },
+]
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
+]
+
+[[package]]
+name = "duckdb"
+version = "1.5.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/59/0b/d65ea3be00ea79aa276a8388bec588a9cbf409ce637c6d306e5316210d15/duckdb-1.5.6.tar.gz", hash = "sha256:166a91dbfacfc0c9f08cc76c0243cb6d3d4296bfab5bad72a3cfb63140a5b7c8", size = 18032957, upload-time = "2026-09-28T13:38:37.978Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d9/d5/d0ab77a0a1702a43171c93874f44c1f6481e30038bd3987df0d77a16a5c6/duckdb-1.5.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:48d07d0651aaeac2c3974afd37599970154b7b79b54c18f27c319c14ccf98d9d", size = 32810486, upload-time = "2026-09-28T13:37:47.254Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/cd/b22201de5377faa3be6c38d5f3eaa504cb480392a448bed6a4d2239469b4/duckdb-1.5.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:79de3dfa8705b1ba0d59e7e3252e40ff399e0afd12f485502a6c7bf7c2fd809a", size = 17405278, upload-time = "2026-09-28T13:37:50.135Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/6d/f9cfb1493bbdc2f095693a402e42dce1192077f9e11573f00baed6a748de/duckdb-1.5.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:dcccce20965e6986cd083fdf192c461685ad0b93cd1ccd0b2a8207f1185f078b", size = 15532943, upload-time = "2026-09-28T13:37:52.927Z" },
+ { url = "https://files.pythonhosted.org/packages/53/04/f65ccfaa5a833f2e570c4a140f03c8f95da416da9fe8ed08401f81f8242a/duckdb-1.5.6-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce89a1025a5317ebe9c520876c48032b5247ac574865486648b1a004f6009875", size = 19454940, upload-time = "2026-09-28T13:37:55.732Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/99/be75c788a492f8d77b7a1cdc1b19939ae7be0007f2028691ad371a1a33ee/duckdb-1.5.6-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bc9619ed7d4ffa117b5155d84b44794366bb6635178d78ed5e13a6024845c757", size = 21568087, upload-time = "2026-09-28T13:37:58.191Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/95/889f8508960e47c0a7c75cc5bf57cde8512fc24f8db7b3129cca5388da42/duckdb-1.5.6-cp312-cp312-win_amd64.whl", hash = "sha256:09ff51b230219f0d8b47fc8a1e17fb595ba9fab0c3d96a6de4d00b8ff86b3cf1", size = 13190189, upload-time = "2026-09-28T13:38:00.407Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/c9/baab503364a68309f8368c88e77f5341e7d94927bdf3e6d703f0e5035f3e/duckdb-1.5.6-cp312-cp312-win_arm64.whl", hash = "sha256:b8d795c8b2d5634b3269f974aa97f1fdf878f62f032317a52252a151b693fb1e", size = 14021977, upload-time = "2026-09-28T13:38:02.682Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/5e/a476197fcba557738a588ec844747a19bc0a24b0e6f1809e308f29d68c0e/duckdb-1.5.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ae352646374cacf48e9981cf031191c494865192fc436d13667a2531fc5d1da3", size = 32810376, upload-time = "2026-09-28T13:38:05.148Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/6d/5466a2b53ddd557644dfa47a763f68748efccdf282e6ae7c4f1bcfb3da69/duckdb-1.5.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a1261e90785e9d29953293e44f60fa073bd1137098924e8de21a037a861b051", size = 17405385, upload-time = "2026-09-28T13:38:07.363Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/a0/bf87071170835ee4a34fe764fc11c1c6e7040a0e021b36c1b6f834a4c22f/duckdb-1.5.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:97dd7a555b8f5298b76bc7d48a11cb2c64336e8de9bfde783cffb86ea9f54807", size = 15533132, upload-time = "2026-09-28T13:38:09.681Z" },
+ { url = "https://files.pythonhosted.org/packages/31/e0/38095c8e140ecfbe847519ac07bcba94301b8fbb76b2870015e33e07f179/duckdb-1.5.6-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:364992ba1089a2b327391cfcb68fd0bd0ce9090cf293baef861a0ba6847abfee", size = 19454994, upload-time = "2026-09-28T13:38:11.836Z" },
+ { url = "https://files.pythonhosted.org/packages/70/21/61dd2876bbaa69cf77d7b5c620e52e8b25faae7096f4d2e4a812b52095d7/duckdb-1.5.6-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:644f54ce99b3b61844bc9a3fe80e0aecb1ea4084b1fffc4396d1569db6111679", size = 21568700, upload-time = "2026-09-28T13:38:14.258Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/4a/100730e7785e85268be4d4d5bd62cfc8314e261d2f42efa208243eef35cb/duckdb-1.5.6-cp313-cp313-win_amd64.whl", hash = "sha256:ced693d33ddcee2e5345f077d342c87d2aaa80e41c514e64c9ff2d4e5963c251", size = 13190707, upload-time = "2026-09-28T13:38:16.875Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/2e/bc7f44eab4e89ee5c1cb427bb1168ad021d985042e6841ec0694c3d3d501/duckdb-1.5.6-cp313-cp313-win_arm64.whl", hash = "sha256:41ecc75bb9328d72d154a705c1a653d2c5c60f686a5c0c6578aa80020753c884", size = 14020962, upload-time = "2026-09-28T13:38:19.007Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/62/a8a30a4c6b94c0861d348ed5633b963f6745a5525527530f02f3c1a7c931/duckdb-1.5.6-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:aa21d2ad803b2524326e8622d7d96b2bb1ff1d5b60368e1978ee805df9c21fb3", size = 32828003, upload-time = "2026-09-28T13:38:21.414Z" },
+ { url = "https://files.pythonhosted.org/packages/71/b7/1dcca0005eb8c67adf9fc06bf0cbb1d2bf4ea1974cc89e7a7c2ad66aac28/duckdb-1.5.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:8a1b2ad27d414068cbca06c55cfa802eece10f86ea4812ff082f8ab4cb25fc85", size = 17413912, upload-time = "2026-09-28T13:38:23.915Z" },
+ { url = "https://files.pythonhosted.org/packages/93/b0/e3ac175443550f3464f2d95731a8b0aae9b4dc3875c3a186c352262b43c2/duckdb-1.5.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c79c6d222b1d015cde73b5139087186b00db65357fb4e2c94c2308fbbf465a72", size = 15543122, upload-time = "2026-09-28T13:38:26.317Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/08/cc510a7952aba69d5cdca17f3ef61c95713d86143f2ee9aa3e097d38f50b/duckdb-1.5.6-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1052b8050ef5696e2c0d8c836949c72f3dd11f0690466acbea739613e8e2750b", size = 19457946, upload-time = "2026-09-28T13:38:28.877Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/a5/6f8099d9a5a02ddff89e5c85875df3465054845b0920fb0703fbdf8dd2ec/duckdb-1.5.6-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:19c5e485e59613b8878d1670bcaa7a010f53c5a4da5ae8e08863e5e529ca6182", size = 21575132, upload-time = "2026-09-28T13:38:31.231Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/58/762f7159662d7859e201fa05ca29f306795daeabf84f3e087215a966b001/duckdb-1.5.6-cp314-cp314-win_amd64.whl", hash = "sha256:ebcbd09cd8578ab1093393e9b16289cda0e8f1791ac595bf00eb5bad75c3cf00", size = 13713963, upload-time = "2026-09-28T13:38:33.543Z" },
+ { url = "https://files.pythonhosted.org/packages/46/69/64d165db322de13f5c3e75d377b6b9694df1821155ad1fa4b14b04601abc/duckdb-1.5.6-cp314-cp314-win_arm64.whl", hash = "sha256:820a8384faef11cd86068ea48c5da57ce2d8f1c7b3d2bdb9be3398317a7c3728", size = 14514368, upload-time = "2026-09-28T13:38:35.676Z" },
+]
+
+[[package]]
+name = "duckdb-backend"
+version = "1.0.0"
+source = { editable = "." }
+dependencies = [
+ { name = "duckdb" },
+ { name = "fastapi" },
+ { name = "httpx" },
+ { name = "pydantic" },
+ { name = "python-ulid" },
+ { name = "pytz" },
+ { name = "uvicorn", extra = ["standard"] },
+]
+
+[package.optional-dependencies]
+dev = [
+ { name = "pytest" },
+ { name = "pytest-asyncio" },
+]
+
+[package.metadata]
+requires-dist = [
+ { name = "duckdb", specifier = ">=1.1.0" },
+ { name = "fastapi", specifier = ">=0.115.0" },
+ { name = "httpx", specifier = ">=0.27.0" },
+ { name = "pydantic", specifier = ">=2.8.0" },
+ { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0.0" },
+ { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.24.0" },
+ { name = "python-ulid", specifier = ">=3.0.0" },
+ { name = "pytz", specifier = ">=2024.1" },
+ { name = "uvicorn", extras = ["standard"], specifier = ">=0.30.0" },
+]
+provides-extras = ["dev"]
+
+[[package]]
+name = "fastapi"
+version = "0.143.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "annotated-doc" },
+ { name = "opentelemetry-api" },
+ { name = "pydantic" },
+ { name = "starlette" },
+ { name = "typing-extensions" },
+ { name = "typing-inspection" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/0b/d7/6a8753ab6c1d432dc53703c3e1b92974a94531b7d047c32bbaae461ea844/fastapi-0.143.0.tar.gz", hash = "sha256:1acffe48206a80917cf7dac21992b5c44b25384e8902bf745c1fd9dabcf6c51f", size = 468391, upload-time = "2026-10-08T12:29:46.54Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/bd/f4/27e386913417ad32aae42bba48b0c0cce40e9ff2fba1a871ca2702c37324/fastapi-0.143.0-py3-none-any.whl", hash = "sha256:3e9395fd35276425b61b516a31fdd7c77fe2af83e41b4da22e30696fb1304c5d", size = 144665, upload-time = "2026-10-08T12:29:44.853Z" },
+]
+
+[[package]]
+name = "h11"
+version = "0.16.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
+]
+
+[[package]]
+name = "httpcore"
+version = "1.0.9"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "certifi" },
+ { name = "h11" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
+]
+
+[[package]]
+name = "httptools"
+version = "0.9.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/3a/ec/deed52912ab7ca6c0b12859330c571c60c61d7267b341b28951fcbf13694/httptools-0.9.0.tar.gz", hash = "sha256:d484ebb7e3a3f3597b0f645fbd1b85633674ca808c1f5ba11c2caf7c66f5c8b6", size = 282523, upload-time = "2026-10-09T19:57:04.301Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/da/ed/0916b8b7ebd1deeaf22acba71b68c57b4b6b69aa1918f3812dea208b4276/httptools-0.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f9ccc9884241efceb4547a92955d128574c864681f11b7ea3ecbde295fafbe8b", size = 119039, upload-time = "2026-10-09T19:54:32.556Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/0b/9b6de4a01a563a904d0826c9069c824b330e1816df26c9bdf93f60b50857/httptools-0.9.0-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:45b3002392948dcf578029c89f6318e1289a993a1a5ec38a4161560fab60f811", size = 115051, upload-time = "2026-10-09T19:54:33.908Z" },
+ { url = "https://files.pythonhosted.org/packages/85/3f/642113e9882f53158ecddf58003d25f18ded2c210ed23bf6eb663d4d51c3/httptools-0.9.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3e3201fe4d46e0d15d7ff9fafc94a605da9eb82d2c5b9837f0368acb325481f1", size = 552848, upload-time = "2026-10-09T19:54:35.434Z" },
+ { url = "https://files.pythonhosted.org/packages/95/4c/3ecc59c99c28652d8d08d9b5be65770a14d2cadc616dad94224cee2b0e7e/httptools-0.9.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:58a1b0ec4cbb930e69669f9771715b2c7898d3cdf064d9811f7a66afef96b544", size = 549181, upload-time = "2026-10-09T19:54:37.099Z" },
+ { url = "https://files.pythonhosted.org/packages/43/ce/21f5b2759590b7054e38d3b704a3c6b853c3395c370b3d6f16c45aea0fc0/httptools-0.9.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4c58dc91aefb31adad500aa68054334f429b840b36dd29e34e834101044cb2ef", size = 569121, upload-time = "2026-10-09T19:54:38.772Z" },
+ { url = "https://files.pythonhosted.org/packages/52/c3/7c523aa8d0fa7a57010a3e1bbdebc209585009076465f3d1ae6a3f54b814/httptools-0.9.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6b900073e7b8481ef1aaf4f6c1789d210a1db01a9da8789821578cfeb4c2d540", size = 487649, upload-time = "2026-10-09T19:54:40.398Z" },
+ { url = "https://files.pythonhosted.org/packages/94/e2/d90d60002692b8afcbc06fb49ca3a4365b32abed6c40fb2b612c67721a00/httptools-0.9.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6c12d0393a903b58bc5f5a7406d6c5290acfb8284290d68547ce620c06f7d133", size = 529583, upload-time = "2026-10-09T19:54:42.296Z" },
+ { url = "https://files.pythonhosted.org/packages/46/c0/19172874cde0344a20c85877a0b2d0dcfca31111729ad8a79e8b4ac4e207/httptools-0.9.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:29b0d823e3c1e7cd1093a5dc889245db693ef13ada624cd66e2262421ef38867", size = 552414, upload-time = "2026-10-09T19:54:44.19Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/b8/02ea7910f69e5371986b025fb3b410592106df54e977a5732fd1d95917b5/httptools-0.9.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:6ebd39ee26db460cfe5ab8b71a15d1149b289139a0d3981522757d6af620887e", size = 482400, upload-time = "2026-10-09T19:54:46.064Z" },
+ { url = "https://files.pythonhosted.org/packages/de/97/f05eac916d44cbbfe43668a6a40ab93e7fd8f94d5120d1ce2d8e55c69871/httptools-0.9.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4efbee349138a3fee7a4cc3a95abd2d499fae70dd5bff9fed9138d6f570f4283", size = 536148, upload-time = "2026-10-09T19:54:47.695Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/e9/9435dfcb7f1a1d6ebdc79a902164dfca70e33774c80bb60d25c630c31ef1/httptools-0.9.0-cp312-cp312-win32.whl", hash = "sha256:36fac804b8cfd6b935ae64f71349f833d2b6298404626d017a2c57bb942bc643", size = 86274, upload-time = "2026-10-09T19:54:49.1Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/69/813f1bf90be507d4166c437be1a413574d0e0abf36e2fec10c266661b0ee/httptools-0.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:7e32b83bd8c2f8b6fa726ef34e63e21c4d7eddc277d40d4ef7245ea3ed28e5b6", size = 92446, upload-time = "2026-10-09T19:54:50.498Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/e0/1d29e328c4cafe843403341e1455e0aec18b0e6910fbb14f12b36b563f19/httptools-0.9.0-cp312-cp312-win_arm64.whl", hash = "sha256:813a32f94991b9627795528053c73a57d2ce3eb98ede89f0e1c7a31095938e81", size = 88676, upload-time = "2026-10-09T19:54:51.844Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/04/223994f8589750d2a36ceb43203e739cf75bd9e12c226680d73567766908/httptools-0.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4fb995082fe41ec410b33c48b54fb1d44abb8a6ee762c31e8c42519e8c3a30a9", size = 117115, upload-time = "2026-10-09T19:54:53.356Z" },
+ { url = "https://files.pythonhosted.org/packages/31/d8/b4407836e567a862ce79d78a628d785db99aba52e63496d68c60eed0d475/httptools-0.9.0-cp313-cp313-macosx_11_0_x86_64.whl", hash = "sha256:b9cd15cb7cf0d5cc41f649fd789aae12c56c3b83eff593f8e095c1d4555ad5c3", size = 113225, upload-time = "2026-10-09T19:54:54.81Z" },
+ { url = "https://files.pythonhosted.org/packages/79/f6/0caa51b077492a7306bdbd9dfb907a2246985f0aed1fe2d086255921848b/httptools-0.9.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:088de1738e1af624466a01c35d652dbe6fb825be887c76d68aa850621d81db88", size = 520112, upload-time = "2026-10-09T19:54:56.3Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/da/7a47b7c2106bb10e6d4c04a139d045257a4f93c672fae6f0b9e92b1f7bc2/httptools-0.9.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b1ac7f1bc6c0dbf90684b77571a51a21b2463909fd916ce0ac9bfc4d566dc75", size = 516079, upload-time = "2026-10-09T19:54:57.938Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/4d/417b42d2663acf4f5aeb2718dc894ec2be4e3dcfd8caa2d3bf9ee2dce511/httptools-0.9.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b9430f65db521db7962ad951571d446171213686f96c998a54dc18ed574821e2", size = 535040, upload-time = "2026-10-09T19:54:59.769Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/de/8df4c09a33ddaf50f697719f20201cf93631ef4b50cec05e42acf179a7c1/httptools-0.9.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:52fe0176682a25b15370f23f5b0f1366a84771df89144fb0cd979cb72a94b5ca", size = 462799, upload-time = "2026-10-09T19:55:01.673Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/90/1bfe91e3fca29c541d85d7ba8ed92a406d4dd13608c281baf7ec75369fec/httptools-0.9.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:757e3f79cb865a7db94e0db5f4d0ed3284a69e39d53568f433982ea13c60cac1", size = 497596, upload-time = "2026-10-09T19:55:03.201Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/af/2bbd5af0dd7a0e0c3b63bfefafd87a07041eb13d7cd710fbf30708b70773/httptools-0.9.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:6ff5f0ed70783dcb9562dbd20edca51c3d4d277f128223709e3da6b75986d1d4", size = 517110, upload-time = "2026-10-09T19:55:05.011Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/7a/9f165817c3e27df9098f3d50a675417d8721253f1073434f48a3f9d9a6c2/httptools-0.9.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0f537e5e8152e8d9cae82804024790cb973061abd3b7ef8f66f46e2b5c7bb51", size = 459198, upload-time = "2026-10-09T19:55:06.985Z" },
+ { url = "https://files.pythonhosted.org/packages/93/20/b93279e334946c359d39aaf405241c6fd60f9e60da709bc4156731a4413c/httptools-0.9.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1a7f1df31829c258158be01bb04eb668c4fba7df1ddf2262131a972962e651b6", size = 502996, upload-time = "2026-10-09T19:55:08.733Z" },
+ { url = "https://files.pythonhosted.org/packages/86/c9/ac3657943d40c5a9949b72565ee03151e480fb18c062c7c13c0c0276df6f/httptools-0.9.0-cp313-cp313-win32.whl", hash = "sha256:714bf348f468532d86bed670837e7d5ddff3834dd7f5d3c08066da400c86f088", size = 85878, upload-time = "2026-10-09T19:55:10.275Z" },
+ { url = "https://files.pythonhosted.org/packages/74/69/d23079cd4bc16d11e49c3f51c2540c018736f26701a2a73183cae9255a1c/httptools-0.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:805b0f2618e5d4c3e28f45b731eb1a0539691ae4a2f97b4ce014de0bf96a1ff5", size = 91549, upload-time = "2026-10-09T19:55:11.701Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/ed/5ff678a774b721f054c095f04d84fc536e7369ea4f4c9af3813a518d95b6/httptools-0.9.0-cp313-cp313-win_arm64.whl", hash = "sha256:bfdabac0c6d3d6a5be8c2a100a001c92c14a39bbafd5999545a675c493626e64", size = 88043, upload-time = "2026-10-09T19:55:13.046Z" },
+ { url = "https://files.pythonhosted.org/packages/31/39/0965023968452245ece67b161adbf7c5652f8d0697ac69312f9d21849411/httptools-0.9.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1a4050a651e1f2faf05eb028ce9f2168abbcee9e24b209f5c1f2eb96d8c569e4", size = 118142, upload-time = "2026-10-09T19:55:14.491Z" },
+ { url = "https://files.pythonhosted.org/packages/31/39/a6ec662d81059e505e953af709797038e83e489014df721e506f4fd0d3c5/httptools-0.9.0-cp314-cp314-macosx_11_0_x86_64.whl", hash = "sha256:130635fea6e611a6b2026120037965ddb88b3dafd11bb64e264b101a70a76630", size = 113943, upload-time = "2026-10-09T19:55:15.887Z" },
+ { url = "https://files.pythonhosted.org/packages/72/04/4ecb7251a6c55bef61b157bb93fd44678943c35702a5966e4d5ebda2d450/httptools-0.9.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:18d800aaa2d6bff7d889df810d1b19a5fde72b1f6c0ca96e8d9f28a692fe5460", size = 514397, upload-time = "2026-10-09T19:55:17.48Z" },
+ { url = "https://files.pythonhosted.org/packages/31/5a/0c26c98ee06f0f39608de715e7ca868baec942171a77feace5a0ba548ca6/httptools-0.9.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0e45def4d9ce7073e2226535572442d9d6efb4047c7a5fd8960807e877ce70a", size = 514383, upload-time = "2026-10-09T19:55:19.221Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/6c/0f85d4f1f579c49aea6e4946dd304e9f33a680382b5117970ab887885bc7/httptools-0.9.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1f6da814aeecbc6cb8872d6d3e85ed16e8ab1653f9557cea8658725ce212348a", size = 534993, upload-time = "2026-10-09T19:55:20.992Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/32/97a836533b7bc9e269fc6d075c2d27669ca9786bf43f229158b9b4b15021/httptools-0.9.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8e1e037bb57dbc549c6fe20370b763ea74bdb09413cdcf857e4f14d9e4e2fb13", size = 461494, upload-time = "2026-10-09T19:55:22.785Z" },
+ { url = "https://files.pythonhosted.org/packages/67/cf/a2d5e8dc3bad9b0b966bb546170234b4614275346cccbc01f6cdb6fce3b3/httptools-0.9.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cd3e55223a77d6e08d5730ebacb4930ecca5d2ce7c57e7ba10833be7e52903f1", size = 495859, upload-time = "2026-10-09T19:55:24.9Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/d9/7472c4ca2aa1cfe6d0f9923380784b034cb77addc88589f2e5c92fd3b4df/httptools-0.9.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:beb2c8a34cc90fb4d862b7284eafdb322030d6a8b2ee5eb6a744f84205beedc3", size = 516303, upload-time = "2026-10-09T19:55:26.84Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/dd/f9be002ba859714cc306fe86204b7cb12bac091be66a7e23d7bb25d259bb/httptools-0.9.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0cc339a807c156d840b54f8bf050ba0fc265eb81692c24bca8535b52fbd797c6", size = 458188, upload-time = "2026-10-09T19:55:28.571Z" },
+ { url = "https://files.pythonhosted.org/packages/89/7a/ed8bb5344071afd12c87e57e8839fa65abc3895b92a5d065be79ecacb919/httptools-0.9.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b6ee42112d785a913dd63ec0335435a3dddbea5040c151252db815b0095cf066", size = 498356, upload-time = "2026-10-09T19:55:30.301Z" },
+ { url = "https://files.pythonhosted.org/packages/04/8d/3f1390c901d4a266ad9d5b988c47c4883e322e6f6cc021c592b9a050fb19/httptools-0.9.0-cp314-cp314-win32.whl", hash = "sha256:d1e329a1866981efe0201d05a374617f6c6cf14434a501d78ab22793d1ab1fa6", size = 88479, upload-time = "2026-10-09T19:55:32.071Z" },
+ { url = "https://files.pythonhosted.org/packages/99/05/7de70a4eea3b52d31a95fe64eb5775ccdead01e4913e4741b4424e9ef180/httptools-0.9.0-cp314-cp314-win_amd64.whl", hash = "sha256:edd5aa045fa3cc57143db018dd32ce7962bd5b525d05230709015d7e570100aa", size = 94809, upload-time = "2026-10-09T19:55:33.423Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/79/7f6c354a8f8f74381fd473f365d2db3cd976ee8d1422b8dd7455dfc52b62/httptools-0.9.0-cp314-cp314-win_arm64.whl", hash = "sha256:6ff0145b34610e57c9fae20df4e133c8d54266447387de6fcc0bdabfe4db4569", size = 91508, upload-time = "2026-10-09T19:55:34.764Z" },
+ { url = "https://files.pythonhosted.org/packages/94/0c/f9e8148ca684b41b4b5d0ced0860530b9a9bcb7c38bf727d83dcbfea42d0/httptools-0.9.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:80eae881cfb69383303e9a4d7961a478025b89c24f38f2e69b30c516fa0d57f2", size = 123684, upload-time = "2026-10-09T19:55:36.445Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/54/3c1d910e8f0bc9ee0ba7867b687e3272c8ae4a7da2df2fbf1b2bce77f0f9/httptools-0.9.0-cp314-cp314t-macosx_11_0_x86_64.whl", hash = "sha256:b2ab3aad55d75d0b8df8d8a1b5920baaec9b161112cd5e95984848b4d2cd3dfe", size = 118378, upload-time = "2026-10-09T19:55:37.851Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/ce/3b9694880da927ae69b5629b8847cfe73d14584be2aa974a92ed2675b7da/httptools-0.9.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:db735a23ecb0f0450d2b24e0a05fb00a8a35c9db172919c4d3e023e7c7ee4c9b", size = 591295, upload-time = "2026-10-09T19:55:39.501Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/89/1ff2835b6adf5c08a477d3a199e72b71e7f26df55ceaaed7d7364d745a1d/httptools-0.9.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:995b52f7c260ac7023640221f27472303968753cb6fc6fce1ddfb0e9db59a398", size = 603709, upload-time = "2026-10-09T19:55:41.404Z" },
+ { url = "https://files.pythonhosted.org/packages/24/40/4f59a0d9dca6d60002e7cb5dbf1441b558ced5a65b5b4131d57cbbd7c806/httptools-0.9.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3af4e45ff455fce5511fdf2653c1ce428ef09c56fe37a83eb4d924c2d474f31e", size = 607379, upload-time = "2026-10-09T19:55:43.119Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/19/381d444a3ba704cd5c67eb4617ae7a08e920a8239c688f23ba0de07a270b/httptools-0.9.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ce8e723b4637034b76f5382a30a6b725518c332273e8d62a6c7d46e90837c947", size = 530031, upload-time = "2026-10-09T19:55:44.85Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/c5/c9ba7758bf266240f598934510af4a800edafd9c8eb1fcf15feac0427063/httptools-0.9.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:465bc1526debf53a3be92022a16ca0c38f891ea3b5c1587af4f52e44020f8a07", size = 575129, upload-time = "2026-10-09T19:55:46.536Z" },
+ { url = "https://files.pythonhosted.org/packages/db/87/c17f3a53616a3849681f7c8e913ce966487b95038504bbb035c38f5f2fbe/httptools-0.9.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:8463b34ebde3f000627e9dbd8a545f995ad49fbf7ff9dd5abc0cd507da98a603", size = 582996, upload-time = "2026-10-09T19:55:48.545Z" },
+ { url = "https://files.pythonhosted.org/packages/88/e3/cb33ba1348ddfa5853f96021f4c38674ac383b92c944492cf7638bd6bfd0/httptools-0.9.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f9489c1d87160c126f73b004742fe8654fa1ce37ed89e9e01330a1c10aaecde4", size = 526150, upload-time = "2026-10-09T19:55:50.261Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/00/af0e2f33ba5be60803a492ad377e798714d0c970e76015e313849b351ef7/httptools-0.9.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:06bfe7fad972a417269d8a5fc53b87e4eca970354abf5e9e24336fd06d64292e", size = 571731, upload-time = "2026-10-09T19:55:52.422Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/35/e67e9c9dd3da036ebfcbd273eec44bd39213f952d638858b09b9f3ecaf3f/httptools-0.9.0-cp314-cp314t-win32.whl", hash = "sha256:c42424213c28804f8d0e20f5692106cfb57bf72e1dbc4092b8481fb2f9e4c707", size = 94622, upload-time = "2026-10-09T19:55:53.982Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/5c/af620c73de59b5f3d431ae778c7412d30bba7bf56ca8b4140107a8ac0e54/httptools-0.9.0-cp314-cp314t-win_amd64.whl", hash = "sha256:bb1533541c729ad422f870a780d8b4af924f9817d45b5f580390418cda72eaa2", size = 101934, upload-time = "2026-10-09T19:55:55.417Z" },
+ { url = "https://files.pythonhosted.org/packages/90/90/fc6019b5179d13007c6c3039346ea2696cf2e94369d6ca96e57f23b01989/httptools-0.9.0-cp314-cp314t-win_arm64.whl", hash = "sha256:6f9549ca354a1d6d6167c458a1f1b12147726b968f02dd64b6a5801dba91ae0f", size = 97211, upload-time = "2026-10-09T19:55:56.878Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/77/e226b16a2f291f2a4ce25a24a3297e98749d80b8a713b8f3b11d8a82e904/httptools-0.9.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:d3906b5c549ff2ad2473cb711e1fc65d76715c2726a402108fbf55eab6c6b49d", size = 117817, upload-time = "2026-10-09T19:55:58.295Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/08/050ad8985ec34064e4401e6e5aeca7238685bc218eaff20025f7c04b0723/httptools-0.9.0-cp315-cp315-macosx_11_0_x86_64.whl", hash = "sha256:cb2bb3ac0af7fdab2311b895c9eb95442b45deb14cc949b9e65545e74aa0be69", size = 113669, upload-time = "2026-10-09T19:55:59.915Z" },
+ { url = "https://files.pythonhosted.org/packages/52/0f/af812488a4963ce59d97b73a00c72bba49f5eebca1a13ab6f114372b5e82/httptools-0.9.0-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:63d38e9a9a10a20fb57593742e63c6b1e78dd7f6ef5472de8e0b1e4cf4f3db26", size = 515633, upload-time = "2026-10-09T19:56:01.529Z" },
+ { url = "https://files.pythonhosted.org/packages/50/6d/73c987b84e0d02fa6c4109c7ce6ea00518d0aa3005fb92b75553ffd5ddf8/httptools-0.9.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eae4e9c7a0785a1a715de0a74fb822ab40084c060f444f18f075d05e322aa7ef", size = 516049, upload-time = "2026-10-09T19:56:03.327Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/f9/74cc01fba5a0ea05501eb39eddba4baa00c10e4d1caebdb78f23eaacafe5/httptools-0.9.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0adc974916efe1fbf89d0363a86dcb2c746727643e362ff398de1a4b50b6bc77", size = 534832, upload-time = "2026-10-09T19:56:05.068Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/a2/a7bb90643c059e8136c2a5fdfb0d7e1a18b2c5c4f1a78f2de14b1303184d/httptools-0.9.0-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:050f84b7ec46a6efe0e5f521cf8729e3397c1cef4384f62ed8d5d68ca0045776", size = 466489, upload-time = "2026-10-09T19:56:06.757Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/19/bb3f18e05cbad9628e7f1254176c475e05ac79c72697ec7c144fc2cc877f/httptools-0.9.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:9b4da5789d7cf576c7e81f0088c632f6ee3786d87d17f08e90e703c22ce15633", size = 497146, upload-time = "2026-10-09T19:56:08.641Z" },
+ { url = "https://files.pythonhosted.org/packages/25/e6/90e2433d7a947bec66a5ad22e948626a26672ff62aa3ebf949899f687a3e/httptools-0.9.0-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:f78f7ae1c2e5aabf29583fc0d302d8081a663776f84578025662eb6f5d63a921", size = 516153, upload-time = "2026-10-09T19:56:10.415Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/c7/86373edd9d800eb723b8b68d3fce0e31d3e3211f9d7b0eaf8c3deadfada0/httptools-0.9.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:b2cc6991f16f6d666d48e4b57318104e7b29109e32e2f6b86e9d44c4e6a27f4e", size = 463141, upload-time = "2026-10-09T19:56:12.406Z" },
+ { url = "https://files.pythonhosted.org/packages/65/46/8dc41d9ebf78fa56f609f251ed8ac5a9f66513b0ce712040bd7ada7b19cc/httptools-0.9.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:dbc9fd1521e573045d71b6afab7398439c5cc259e8cb9d416fe62d485c4899c6", size = 499125, upload-time = "2026-10-09T19:56:14.109Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/41/38db94fda8b266dcde50722a4fcef825b189380a220e02c682518bc1b430/httptools-0.9.0-cp315-cp315-win32.whl", hash = "sha256:34266cec8c1d4e3e91fcca7efe38971d6bdda64a7944f2a46ab576da15173680", size = 88370, upload-time = "2026-10-09T19:56:15.873Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/cd/347f12eb16e20972dcdacbca907f2c52d72a36542199a5bf3ca342c92098/httptools-0.9.0-cp315-cp315-win_amd64.whl", hash = "sha256:b5a3f5f70967a1aa2bc47fec42a1e19d2fb38c61700e3ee62b63a4af4f4fd001", size = 94617, upload-time = "2026-10-09T19:56:17.257Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/08/086ba2f53989d504a05f4669b03673a04fc72554bc37d4696c3c6132be75/httptools-0.9.0-cp315-cp315-win_arm64.whl", hash = "sha256:e0acbd474d0af4afacc6e66c4273f8a19e25f8af4379fc816388095ea6b01371", size = 91358, upload-time = "2026-10-09T19:56:18.641Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/3a/9ba59ec76d45bf8eb7ad3a18f2c6e9074fa4ce5cbbd3900fffb8d840f9e7/httptools-0.9.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:02bc5b3dcb6394b9d825fd62a7bfa0b2943063a3c89abc4492ad45e334a20eb5", size = 123063, upload-time = "2026-10-09T19:56:20.023Z" },
+ { url = "https://files.pythonhosted.org/packages/18/2d/49eb389bda75a8ef0d04bf025dfb8412a3646637051c8a88bdeea700e343/httptools-0.9.0-cp315-cp315t-macosx_11_0_x86_64.whl", hash = "sha256:fc1a4f9d18d32a6e0a0a0a382986a60a2126f5144dd08715be7adb8df18e8a46", size = 117328, upload-time = "2026-10-09T19:56:21.439Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/6b/2d6439378fd3d1f9c06272b35d61f4519e2d9bf9967611df069fa6c23044/httptools-0.9.0-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:df3867518b205be3648e2fbd522bf380c851b5c2500588047505afdd786b6669", size = 588680, upload-time = "2026-10-09T19:56:23.056Z" },
+ { url = "https://files.pythonhosted.org/packages/08/65/3fb50e861bbb6103ca58fd88b4127d346fc909eb9f06d250455033a3f698/httptools-0.9.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:26e1d9629f3bf70d23f0d22238152aec51c837a7c9e384cb74f356fdccad7eb3", size = 600031, upload-time = "2026-10-09T19:56:25.216Z" },
+ { url = "https://files.pythonhosted.org/packages/90/9b/40d33d4098fde007845804b1c923ddf5a27fd48aca1c8080bdbdac6c16fa/httptools-0.9.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:050f7ab098121873c8f13e35857f97ab60a76185c8302bde9a384939bb7c3b96", size = 604407, upload-time = "2026-10-09T19:56:27.04Z" },
+ { url = "https://files.pythonhosted.org/packages/17/37/472afc9000aca3c7dd61a9b8ac6f3e2765900e3614f8d7f13e772c9c5438/httptools-0.9.0-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8d90d10e9b6594c28f27896a68fab97fd784c43804e9fe419dab8e8dcfcf4b02", size = 529459, upload-time = "2026-10-09T19:56:28.944Z" },
+ { url = "https://files.pythonhosted.org/packages/88/f9/9956910fb1d181578249cd2cc966c0c46ad3c558b43ac2b79af50f94589f/httptools-0.9.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:b928ab0ecaa664e8caecc529dcb8bc881b6b35bb2b74bf9a39ae25f982ee8812", size = 570845, upload-time = "2026-10-09T19:56:30.602Z" },
+ { url = "https://files.pythonhosted.org/packages/30/8c/d1c160a3cc2c18e41a6f763c3aad979530dfb295039449312b8814e19753/httptools-0.9.0-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:2319858018eedd0c0b2f950a620413c0a9d1352607be4267eb28209eca8b1e3f", size = 579194, upload-time = "2026-10-09T19:56:32.353Z" },
+ { url = "https://files.pythonhosted.org/packages/90/3c/3f7cc49925928a8c82f4141d504b8b8c2901c4b35cb88800211828312561/httptools-0.9.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:931f45f84e15daafec5f82cc92e6710569e1f50933f3253d206eab4132bec678", size = 524950, upload-time = "2026-10-09T19:56:34.103Z" },
+ { url = "https://files.pythonhosted.org/packages/19/98/8e2154e99b8e8818fad3e6c5dd7cf21c050f6314b1bd8072e8dc29f49eb5/httptools-0.9.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:f67db0ba2bedafec15b8e5330d40da1e1c7921559fa715af021252bfef81a6f8", size = 568603, upload-time = "2026-10-09T19:56:35.876Z" },
+ { url = "https://files.pythonhosted.org/packages/79/a3/86fe9fef3a1bfab5db62262f8880c294cbf8a8d94cffe2a2aa8b4aeed40c/httptools-0.9.0-cp315-cp315t-win32.whl", hash = "sha256:2095207b75a83c9e947346da9c127fb7e4fb29f41589df2643764f06b750989c", size = 94042, upload-time = "2026-10-09T19:56:37.441Z" },
+ { url = "https://files.pythonhosted.org/packages/54/4d/f2d88782251467325a62ec4ad704249bb1b09c21aacb997181a9f4421f30/httptools-0.9.0-cp315-cp315t-win_amd64.whl", hash = "sha256:bca180cbe84e4fba7807eb408a8655295f697928512324517e30a091ede522a8", size = 100837, upload-time = "2026-10-09T19:56:38.831Z" },
+ { url = "https://files.pythonhosted.org/packages/00/4b/5e96c4e0d171f959a0064971c3fced9cea5a19e5fab7a8e7d57aceb80506/httptools-0.9.0-cp315-cp315t-win_arm64.whl", hash = "sha256:4a4d8c2c7e73ba5967be74d7c3a5ff81fde815ee1b48d9c5c0f14de8463a847b", size = 95947, upload-time = "2026-10-09T19:56:40.562Z" },
+]
+
+[[package]]
+name = "httpx"
+version = "0.28.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "certifi" },
+ { name = "httpcore" },
+ { name = "idna" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
+]
+
+[[package]]
+name = "idna"
+version = "3.20"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f5/08/8eea9d4b8302028f3abb2c0813953f7aec26d33b7a8960ed760e65ff29fa/idna-3.20.tar.gz", hash = "sha256:a7db850025b95ded1eae8a46181a1a6c56c92c96f0e2b005d9ff8dc0210cab44", size = 216463, upload-time = "2026-09-17T14:11:04.752Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/58/a2/bb081bab032533a855d44de1d56f8e8426114ff1ba5d1f07a438a0a654f8/idna-3.20-py3-none-any.whl", hash = "sha256:ab7ae7122974553370f0bdb919e1a960b2cd1bc1ef0276416d896db81c14582c", size = 69583, upload-time = "2026-09-17T14:11:03.168Z" },
+]
+
+[[package]]
+name = "iniconfig"
+version = "2.3.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/01/e1/2069291243c926a2ff1cd706c7f3eeb9b62144bf60f77c9fb9ff2fb26bd3/iniconfig-2.3.1.tar.gz", hash = "sha256:67f4b9c50da0dedf52af349e7749a80a9057a5031199791b906c3bb3ae878960", size = 21209, upload-time = "2026-10-06T22:48:38.076Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/56/43/4ca9e49d27a1fcf6bece6f6aec0ea46bb9112489b93d4b688fb415457bdb/iniconfig-2.3.1-py3-none-any.whl", hash = "sha256:9121e2c1fdb355232495be3194c8dfe87ccc2d5dee45947b78e68f499790d7a7", size = 7552, upload-time = "2026-10-06T22:48:36.959Z" },
+]
+
+[[package]]
+name = "opentelemetry-api"
+version = "1.45.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/2e/02/6e0ae9cc61bd3169d401077b507b3ebc344745171e1051ab430be012dcd9/opentelemetry_api-1.45.1.tar.gz", hash = "sha256:aa38ed19bcc084ba42782a73255b3582283eced7ad6dddbd6695189e69adfb75", size = 72804, upload-time = "2026-10-06T17:32:58.133Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1e/41/f7dcf80b81ee8e71c1a2b59f14208bc723edbd89ed027a73b175abf6348e/opentelemetry_api-1.45.1-py3-none-any.whl", hash = "sha256:b31553efa588ae44bc306f863c785c5333a9ecc091248c6ee68b4b6c87fdedfb", size = 60256, upload-time = "2026-10-06T17:32:33.506Z" },
+]
+
+[[package]]
+name = "packaging"
+version = "26.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" },
+]
+
+[[package]]
+name = "pluggy"
+version = "1.6.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
+]
+
+[[package]]
+name = "pydantic"
+version = "2.14.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "annotated-types" },
+ { name = "pydantic-core" },
+ { name = "typing-extensions" },
+ { name = "typing-inspection" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/7c/0b/8e10b2e693af8ec54346a14caf36221334775975a747c9ceaa3f8371d96d/pydantic-2.14.1.tar.gz", hash = "sha256:94f478203dd03404682a1ada216965651dd74b1d2d5ffd62e00e0837caab5c26", size = 900362, upload-time = "2026-10-11T18:37:55.396Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/35/ea/a56b9fe5066f3537b7882f77e9c5dfb26d8c2eefdaed9b5fc73d57b4dc22/pydantic-2.14.1-py3-none-any.whl", hash = "sha256:9195d967ec791692a04438115466764fb8b9a27b31f14a760437694f40d6b454", size = 488106, upload-time = "2026-10-11T18:37:53.437Z" },
+]
+
+[[package]]
+name = "pydantic-core"
+version = "2.50.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a6/24/af4ec4be49fbc810f35b0bdcacc3d433b56bbfa469fd3bfd4ae116cd9bf1/pydantic_core-2.50.1.tar.gz", hash = "sha256:e50d7b94baac6c7d09927fa5ca5800a0c7ee5015c7fcff65beb3a1931b5a6e09", size = 513515, upload-time = "2026-10-11T18:35:44.82Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ee/94/101b49a6c63f99586755690696eae14532772587d9e39e93f7b8d28a49da/pydantic_core-2.50.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:704075d10b74f2f3c6e15407c696d88701df35fc8953f434a431add0d0074db0", size = 2222428, upload-time = "2026-10-11T18:32:20.228Z" },
+ { url = "https://files.pythonhosted.org/packages/db/a3/f8b1b39349c33037c73d7a1b794c9c29d8f07d5bd2b74399c1375a529cc6/pydantic_core-2.50.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e8e1d6ce820aa23317e8209a86bd65a540973c12dc7552b48a4f6c8e9926815e", size = 2025779, upload-time = "2026-10-11T18:32:21.723Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/2d/862a7d115305cc621358e51ba13f296e0ffdf4fbd984be9f63bc7862730b/pydantic_core-2.50.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c18db21573bd2c6489f9a544b7499f0df2853958c568e5e783536ee1f690af41", size = 2048429, upload-time = "2026-10-11T18:32:23.3Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/7d/fed95d18bf2b5abe2d5ab2bbec4184bfc22b6805994a33ccdae8edffd25a/pydantic_core-2.50.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb57f304525a5e3c13333b772bf9a473f36326e9c821b2e8e1b2fd36f80ae2c3", size = 2173274, upload-time = "2026-10-11T18:32:24.697Z" },
+ { url = "https://files.pythonhosted.org/packages/93/1e/c6f22ceac65dc3d291ff79bea0eb74cbd1de8b128f01fee48af03bcea1e1/pydantic_core-2.50.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a27c09d86600f1bf2fe3f37e1ae697faf3143931c09322cd799da94deee923b5", size = 2360403, upload-time = "2026-10-11T18:32:26.281Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/97/190b5b6bfe4c72f265feb657913eb30749193927a5ad7b83cfa7866a25a2/pydantic_core-2.50.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:46b3301d3b5c886f77de7546e47274a5842c622ea2020b8c6524c6b66913b4a6", size = 2439799, upload-time = "2026-10-11T18:32:27.844Z" },
+ { url = "https://files.pythonhosted.org/packages/90/24/48cd98388e4ac2c08d0800af36d15413db15e20387f1038123a2c86cb2f6/pydantic_core-2.50.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93ba4e9d8210d941c200431a56b2c0400b131865947903937ed3ec5404307d2e", size = 2160157, upload-time = "2026-10-11T18:32:29.553Z" },
+ { url = "https://files.pythonhosted.org/packages/00/86/d909133d0b9979ed14c20f110f21124a0e346caf071bd66ac5af3009062b/pydantic_core-2.50.1-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:e5faeaee74a57d32b3ab3aebad2e348f06d3ba946fc5d28c1728455f00a3d13a", size = 2229523, upload-time = "2026-10-11T18:32:31.027Z" },
+ { url = "https://files.pythonhosted.org/packages/90/31/4b43f08131b2098e73a852fd932063385f3103b33839fd9d1abdb89e673d/pydantic_core-2.50.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a3cda0e538208e5d722bbf3698b24f19c0a7d05bc8d5f8a7f9b121ea7fa243d9", size = 2242700, upload-time = "2026-10-11T18:32:32.622Z" },
+ { url = "https://files.pythonhosted.org/packages/88/77/de39658569dc7a4c293d110a89629f61145e7538f00d1e4280c28debefd3/pydantic_core-2.50.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:57f51b31ff826e2859120cf4737c5a758a48d96f3e97da40ccee1796d58078ff", size = 2278452, upload-time = "2026-10-11T18:32:34.165Z" },
+ { url = "https://files.pythonhosted.org/packages/46/bd/1eeb9e4e791e81210ef9096f58080b32ba15b5b13c108d50aaa3f8ce9f73/pydantic_core-2.50.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8daa7ee75245d43ad7d747e5c9ecc1b1d06552f72b14887e9276f787d57375f4", size = 2450098, upload-time = "2026-10-11T18:32:35.547Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/e6/b2accfdff17e5a88d6358536702146de00f9339adaa4d5bbe1ee93cb4832/pydantic_core-2.50.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:acbf31f37c53a5ac0c34706c80b4f5107ba20b05fdd3816124bf236ef0c57dd2", size = 2476839, upload-time = "2026-10-11T18:32:37.119Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/7e/c8a83eb5d2cd42c2e13507150fb777b1edea5a018adc9cbf2858a1d08285/pydantic_core-2.50.1-cp312-cp312-win32.whl", hash = "sha256:45b11cac094aa25725581d9304eee93c9028516b9ea80dd9e175e13a5a2c840e", size = 2101425, upload-time = "2026-10-11T18:32:38.593Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/17/8bbd530b8659e9d963f5b16f6cb8e159f4cd174b4d6304e48969ab12dba8/pydantic_core-2.50.1-cp312-cp312-win_amd64.whl", hash = "sha256:132529c83901437ff642f585216831bf5fd7a91df66829907e155192ead62498", size = 2172522, upload-time = "2026-10-11T18:32:40.112Z" },
+ { url = "https://files.pythonhosted.org/packages/af/9f/a79d690b127e4e3cff143d031538ab9aa715f6b2fbeecbd07cd6f2bf9fb7/pydantic_core-2.50.1-cp312-cp312-win_arm64.whl", hash = "sha256:4e834f6a8e4ff772dcc34f58ef5504147a3ea5b0f4eeb13b0f8eb2ca75ac57f1", size = 2161366, upload-time = "2026-10-11T18:32:41.606Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/57/0e237d7091d2cd44a35d243b7344227f90440166860469cd036afc23a04d/pydantic_core-2.50.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:d5e062c01286d861fd6a1c4ff6e063547b3e713067f2df033c0ff97ac2ca006b", size = 2222774, upload-time = "2026-10-11T18:32:43.103Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/b9/c720e56858d4e1539503297ed37063e0c08e0f3541c41b777f6a800f75fa/pydantic_core-2.50.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0c003c3b7f49debb893d2d85ae099ac5959c9839e2f330fadb1fcdf7a6594482", size = 2024738, upload-time = "2026-10-11T18:32:44.587Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/b8/fbfc25875219cc060e613170ff10e850c6d8924beb4910da57c2ee3ba1d2/pydantic_core-2.50.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:409e0ea40ec30d9158f33574fd758e689f6045a0f2596701828c27816ca9687d", size = 2047049, upload-time = "2026-10-11T18:32:46.164Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/43/34210124d504c553688f2f04b48500b131237528ac545b445e0d6d30e0d5/pydantic_core-2.50.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:131059670f1d2444269b8585cb888963994871932447c08b39ac6a51fcfef658", size = 2173415, upload-time = "2026-10-11T18:32:47.722Z" },
+ { url = "https://files.pythonhosted.org/packages/65/cf/6e178e8fdc11da5965bef980983bf46326a42f436871dd51ba0a57f39df1/pydantic_core-2.50.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6dbcbee53bf17196a7f745aa9bf5a9603953a1e365b1f020be3207c676a3e7c4", size = 2361719, upload-time = "2026-10-11T18:32:49.217Z" },
+ { url = "https://files.pythonhosted.org/packages/11/14/bd5169d356aa91bf777e28ba0b281c192d286d03c2812c9c9db023a2f00e/pydantic_core-2.50.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:325c23f3e35cfbf0fe3486fa5f7260d1e45885173002d30a28ca019994124255", size = 2439803, upload-time = "2026-10-11T18:32:51.025Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/bc/d79d000e5203ebef39af839f2ce77a777fcad6e08ad26af9a2fcd114ffc8/pydantic_core-2.50.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17e722e156d0444ecaefbe640bdb60928752bf2013e2b7a11cdb099aaae19bec", size = 2158402, upload-time = "2026-10-11T18:32:52.714Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/a5/4902cb5fd599422c130bd3124ab31ee8b771199bd56662702eed07d3fec7/pydantic_core-2.50.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:aa8224f10880d9bf1b5993988ba153d42a8b4f3f4f511f93b1f09c93ff613c72", size = 2229755, upload-time = "2026-10-11T18:32:54.126Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/f5/c1481f8669f6060d89110c9b1374173fc8767ca276b84f4870bd8acd5e3f/pydantic_core-2.50.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:41bc8237121bd8dc8d888dfd6279fc166ffc88c1f1bf3a8bf00869680533ca4c", size = 2242303, upload-time = "2026-10-11T18:32:55.641Z" },
+ { url = "https://files.pythonhosted.org/packages/04/f9/77fc3c7653ba9b6e42049e17e96b25388187d4a7c274ab1cb07f58ac8419/pydantic_core-2.50.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:45c6266d071c241f2a168d45bf8c54344f0effce35e7e6b73afdec11f3687568", size = 2278345, upload-time = "2026-10-11T18:32:57.38Z" },
+ { url = "https://files.pythonhosted.org/packages/95/9b/0579c5d12e7f2b16b27e6782427987341fcce07b0725e02ddb0b74add0c4/pydantic_core-2.50.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:1deeacb112d14d3f4fcb16b165f7dbaf76c70ba6e82f37ba042bdab51970a0b8", size = 2449947, upload-time = "2026-10-11T18:32:58.896Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/ac/1b677db91eba54cc5922f4de6edc46ba45c2bd712dfdc0130382d158e214/pydantic_core-2.50.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:1c96fd793b73d1b92e65570132505498fe7b21eaef73cdf74e67e5dfba7ac9e4", size = 2476889, upload-time = "2026-10-11T18:33:00.665Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/2a/3a9f6624ee3ea9ccba5249dde11418bb4c35780a7a92608f8768bd3fea39/pydantic_core-2.50.1-cp313-cp313-win32.whl", hash = "sha256:06ead20d39ffd6f2f6f2a8f8a6de67ff8bb1b4f14a8a30e058502514ee2ac685", size = 2101118, upload-time = "2026-10-11T18:33:02.329Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/1f/323f78ddd9d9938aac420c1abb4e8ba799fc8bdab0acc67c0593b837c979/pydantic_core-2.50.1-cp313-cp313-win_amd64.whl", hash = "sha256:7816e98acc08119dc0f340ab167048ecc54126316330c1f0caf7c6756c88e28f", size = 2170840, upload-time = "2026-10-11T18:33:03.919Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/09/8497c52a739ae425c3ac2f7f56414cbc711c67d374346174f40fe2062644/pydantic_core-2.50.1-cp313-cp313-win_arm64.whl", hash = "sha256:c17799a62c142d61b8a3c51752a7cbc87fe2ad4ccfab10e628a77b405075c662", size = 2161633, upload-time = "2026-10-11T18:33:05.518Z" },
+ { url = "https://files.pythonhosted.org/packages/49/33/28b96e81677153715e3eafb9f26663a80841e859fde282a380359b0d3fa1/pydantic_core-2.50.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:1cf41f1ae3fa155cf167a72689ad044bcc1e3c97e064123677149bdfb5dafc4a", size = 2224428, upload-time = "2026-10-11T18:33:07.153Z" },
+ { url = "https://files.pythonhosted.org/packages/94/40/15c06410c9b7b8da5805d27b64e09bd3f900e986728106db2689dbc51513/pydantic_core-2.50.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4df197990c15b5a37c5a277d131d9f2c67de6133f2e5dafd80d9bba4b99f46f9", size = 2023740, upload-time = "2026-10-11T18:33:08.763Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/4e/5eb629f6efc2a27e421d789dd4bfacbb5f6d09c80c28b13d1e87d73163d5/pydantic_core-2.50.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0036473f5583e6a60e50b8b21651511564277a3f05cc5dab8cf579f552cd5f6c", size = 2049435, upload-time = "2026-10-11T18:33:10.366Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/8e/f195aebec49ad12318876ac2368c197a7939f5d52f8e156d2238ef8a4588/pydantic_core-2.50.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:992c3514ec891fa7858099183e4d64e6bd5a5d4ff452fae29df22faa77a006bb", size = 2172975, upload-time = "2026-10-11T18:33:12.368Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/f5/7ad9fb83010cd5ea0948409db105676ed779c4e709e2d3d89b9fe2558794/pydantic_core-2.50.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:739dc730e6be3bd5ec2f4ab5cfc7eb047cc45fc1497b3bafec74ff2ed07df597", size = 2364197, upload-time = "2026-10-11T18:33:14.244Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/fb/bf0aab3e78301d202b82a0322ec968cd703b11fc0623fde9a28708936f62/pydantic_core-2.50.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:32fad3a91e51b6d2039c572db04a5a873260b399f6bd62c3552671fa7a4a2899", size = 2444259, upload-time = "2026-10-11T18:33:15.79Z" },
+ { url = "https://files.pythonhosted.org/packages/45/35/38f6d6564fae57d9b12e5676dfa947e0e7d5c46dbeaa7eb3352261d6d299/pydantic_core-2.50.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42b54c2c90ad348b5e3a85e03e715d572c1fde357ef104cdfe3b03b697a404ea", size = 2159857, upload-time = "2026-10-11T18:33:17.51Z" },
+ { url = "https://files.pythonhosted.org/packages/65/a0/fb0a3ca10f139dcf765b2d312d10cf0f65c57c59993229d00ad12b14ceff/pydantic_core-2.50.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:2df1ff41884de2bc4b307bafd7c40a691094fad2ff8e767e5b45a319257bcf4e", size = 2233954, upload-time = "2026-10-11T18:33:19.591Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/6a/e63842252702aa4ec6e6b7178ca85e541a77459076592c23ebe2d9840b33/pydantic_core-2.50.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe90228920fd8ff2be62622b6bb8a2b11acd65046d50c6b130614b5879605a20", size = 2243876, upload-time = "2026-10-11T18:33:21.393Z" },
+ { url = "https://files.pythonhosted.org/packages/39/25/5991cf8318b37e0dfab47b87541619a1df8501a793cba0d978846cba37a7/pydantic_core-2.50.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:844b869f118e22a41a091bdcedda8a71bc1b0f62c38d1a0c3211cece47e1d8fc", size = 2277908, upload-time = "2026-10-11T18:33:23.324Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/3e/3ee8baaa6cc25a6961c69168cf9ff0f002d56f4e0d541ad6724c18fb61f3/pydantic_core-2.50.1-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:2eb75304506894a281d346220a4f7481a1b8729577c5ed2a05395991966a8396", size = 2449285, upload-time = "2026-10-11T18:33:24.942Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/c7/acbec6deac13fe697a80c275a9b6661db62c4d323bac8a47347b1f39c7cd/pydantic_core-2.50.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6b20a4bffabdad0db2927ac034ae3b8a681b1f7a0182f3e60b479ad2fde21ebb", size = 2479013, upload-time = "2026-10-11T18:33:26.925Z" },
+ { url = "https://files.pythonhosted.org/packages/10/08/21a3f237b264389f6219d053ee78fc1b5c2fd402c1b1cb2b6cb8b85f9834/pydantic_core-2.50.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:99ba9bc2b8062ea0c326a990f7f00e6530c23579de66dd246e72c4cafef950a5", size = 1484400, upload-time = "2026-10-11T18:33:28.693Z" },
+ { url = "https://files.pythonhosted.org/packages/09/ce/077a6d262d12ef09108377ac0717f029f420d773ace63cafd6143af75568/pydantic_core-2.50.1-cp314-cp314-win32.whl", hash = "sha256:cf356f70551d40374eaffb1aa63f1eb6d2006681cbd7a9faea173ce0f4dd7cd2", size = 2103222, upload-time = "2026-10-11T18:33:30.326Z" },
+ { url = "https://files.pythonhosted.org/packages/14/4c/350a2415209c43d670eb71d3c040f332c04a30583d39e311b05c7ac15762/pydantic_core-2.50.1-cp314-cp314-win_amd64.whl", hash = "sha256:d32f3acc081cc3923386d88f422cde8892335e95f034e0104bb4cf9310d9915f", size = 2171586, upload-time = "2026-10-11T18:33:32.139Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/92/9bea6ca96580a0902fed366f064f0829e404f41889b34543279a1162b888/pydantic_core-2.50.1-cp314-cp314-win_arm64.whl", hash = "sha256:bed5163e03b98bc1fa2eb05d74c63d9c5c95d8ed6254985481640fbf5e237dea", size = 2155773, upload-time = "2026-10-11T18:33:33.896Z" },
+ { url = "https://files.pythonhosted.org/packages/86/8c/f121f073cf32bdd5cba7e6230b1ce0ac845b0cf43e44dd597d95af272db2/pydantic_core-2.50.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:9572c1369e9c9da2d64a7b7992c786d90ff295abc93964cfe3125e4290768070", size = 2234017, upload-time = "2026-10-11T18:33:35.588Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/69/2bb2bcd6146cffe98d760a46c42ae71efd5151d9b2f9c9bf6619a3b32083/pydantic_core-2.50.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2005207aafe1231315718bf6ed5d064a7300fb4772754af35ee72fc68159492e", size = 2001016, upload-time = "2026-10-11T18:33:37.57Z" },
+ { url = "https://files.pythonhosted.org/packages/20/b3/fbf854c7d07ec114260c26e9e2071a4381740f9ae09641dbfcbdf2a18c45/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64f6047f62a6c5ae08d0a6afb035667aa2d97c3d20d69762e034c5ea144d92a5", size = 2023015, upload-time = "2026-10-11T18:33:39.433Z" },
+ { url = "https://files.pythonhosted.org/packages/65/20/6de55b2f92cdb614b745c6e9ced639fc4fd7e1e77604825a88bdece6fcbd/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1ef800dd7d85bcdadf4c3076e4c94e43939493558a3b69a1ea830c706d4617bb", size = 2192543, upload-time = "2026-10-11T18:33:41.381Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/d9/19e91c94bd5c405945ce2f15526808aea37e162c253160de8ed7bf70b406/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b0135bcdcaa0f23573f286e4cb5e0fd2962700964ed13df085b85f2b97aeab9e", size = 2388704, upload-time = "2026-10-11T18:33:43.167Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/bc/2e24c8415eae1a25ee5a5484946a917123bad2e9d01689a759236928175a/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0b3a6f334c6a2345ca15318ff894502a90012536404b37c844a976c76c846e0b", size = 2464118, upload-time = "2026-10-11T18:33:44.856Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/1f/945b8053cb061c64e102bcaf7bfb9ed740c0bd4349f9bb978a7d4ddff4ab/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06e01fbbfdb9be777b316a71b6c49efaf4a08b615d0a98d678cda3023f79d019", size = 2150142, upload-time = "2026-10-11T18:33:46.661Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/78/96a3e50bf0d64aaae781107eb9335b7529d05a85793ed6e7241c4cd1d931/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:a29a061fec0b4e2d714f277e70a3a18125ecff803f2fea6eade2f2e53711d112", size = 2245577, upload-time = "2026-10-11T18:33:48.574Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/5d/a6038a0322232758a6ebfa709f4ca14a60bf5b93940cd0b345056a557da4/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f5187624823423e1d1b82b1072ac41dc837389e18d3d0572cc19bbee46cd550a", size = 2263728, upload-time = "2026-10-11T18:33:50.527Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/4b/76ded3333a457a9344c4f2d63f2d82d0101654b05178fa4ddfe7d3627674/pydantic_core-2.50.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:3e46a9eb0a0901dd6275e6b06ac3a464885ef350ec4121fe486869de8053e4bb", size = 2293630, upload-time = "2026-10-11T18:33:52.362Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/00/9eca378335c9c1b72bc779bf6e4c2a82ce784f14ec48a0e87102c9870e05/pydantic_core-2.50.1-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:756d669f04e62ec4148ecfe22be6a4484d9b1181a6ef32e205ebfd200540858b", size = 2468048, upload-time = "2026-10-11T18:33:54.118Z" },
+ { url = "https://files.pythonhosted.org/packages/35/ca/e3832e9cf93651251de43c8c5c029ed680a3c1a0a1b17ee26c81bed08cbd/pydantic_core-2.50.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:c516cc5367ca3448995d42cb994bf3f4c9002d2a7c22eac9622551269ad1b807", size = 2512159, upload-time = "2026-10-11T18:33:55.99Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/f2/773469b5a10a39116a2cb17edaf6d722f017dd8da07161b371465311c542/pydantic_core-2.50.1-cp314-cp314t-win32.whl", hash = "sha256:9d1bed94af6a63835461f3cf7502058eb166c58c4778e11d0f433cfb1bd69e19", size = 2098406, upload-time = "2026-10-11T18:33:58.043Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/42/0bb74f8f25204b259b11ab7c12dc7f180893b6bd706118b385147fb6efd5/pydantic_core-2.50.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c8dce1f1e0e5358b682a6ad3fa5e31b31d4560997b8e61417e9217c8d60f8a0c", size = 2148481, upload-time = "2026-10-11T18:33:59.913Z" },
+ { url = "https://files.pythonhosted.org/packages/47/0d/d801646c9679a4e630e15cf521d4108b93854b42a6e6e02391bd4c6b1095/pydantic_core-2.50.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ceff0acc940be2715bd6ad17b24c0e5304abf44f6efd0f81ee8499e640f9dc86", size = 2174972, upload-time = "2026-10-11T18:34:01.875Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/84/23984b763d8862a02a13d27a44b6e8169428fd85ecfed88f54c29108604d/pydantic_core-2.50.1-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:8a6791afa2245e6c6b180122d105941644f5bd410bb18623b408808cc41a3102", size = 2224599, upload-time = "2026-10-11T18:34:04.016Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/90/a63cf8586abc1d1a3f6d9b18f0224789ebf003851f092ebda3c7c863fd32/pydantic_core-2.50.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:84f34323a61a365b4e9295de6028474754829aaddd59c7bf1a040e7487ef8f3c", size = 2024191, upload-time = "2026-10-11T18:34:05.901Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/6a/f34bff9808ffb4907fbf5f5040457d253cacf2da7fce9efbc99ca1b1a44d/pydantic_core-2.50.1-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23edad659e8dbd8ca7e4e877fe6c81573abbdf215bd25a68b53e1272f58b80c7", size = 2048252, upload-time = "2026-10-11T18:34:07.757Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/54/13f419bf1eb59852003818e25935aaf175687d978f4c4bca70e08fe40a3b/pydantic_core-2.50.1-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a5fce22f1e87d181e924e12da7d81cfe031fb3881a5ddf26ad28f141756ca43", size = 2173376, upload-time = "2026-10-11T18:34:09.592Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/6c/b5a34d24cd0c81669d8f8339d74e6815abcf2f8fb48ab4b49b84c09be1d5/pydantic_core-2.50.1-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c73622ef819328873b53109ee4f77ceb598bffedd02daf916102be3228866b78", size = 2364559, upload-time = "2026-10-11T18:34:11.534Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/8d/d64d6216a8df365082665927ff923f183056f9049fee08e9777c9ac05296/pydantic_core-2.50.1-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce8c25ca38cc0e3d7753ba180808de2c0c8cb24eae0df64491e40921454e9831", size = 2444697, upload-time = "2026-10-11T18:34:13.535Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/0d/1b1149f60a00ea21ba5f70e28acbd40feb4598af414f80f53c921fac07c9/pydantic_core-2.50.1-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7689580e72a642ab5ec64d5f55b2e33636fa43b4ebe63c0c2c965ef307c7d1aa", size = 2159961, upload-time = "2026-10-11T18:34:15.56Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/35/f236549299dcc78e71e945495d7ec67e78d20844d0999c60601685003031/pydantic_core-2.50.1-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:d5c0e32fdbce7f1e8ef4d11f655694bf5f4175c757a9f1dc2be09b8864e5bcf5", size = 2234581, upload-time = "2026-10-11T18:34:17.502Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/85/26901a490522b7f75ef9bb9a7afb73e5bb550f0e1cb5b99a0069183e2eb9/pydantic_core-2.50.1-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:40f523349960fa30f3ea51404308ff50f9997a90df639590f47a057c1f32b415", size = 2244090, upload-time = "2026-10-11T18:34:19.402Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/2c/481bcfc70ceeb77a778ca6e5b705fe592cb64735c108157f81b7dd9c280e/pydantic_core-2.50.1-cp315-cp315-musllinux_1_1_aarch64.whl", hash = "sha256:d4193206b6587047437f6f11d7e776df23e1c1e23af2a54d9347275614791e10", size = 2277948, upload-time = "2026-10-11T18:34:21.317Z" },
+ { url = "https://files.pythonhosted.org/packages/24/eb/f1e09333faa7ba447cde967310f758430cc7c5e987bdab5228816c70030d/pydantic_core-2.50.1-cp315-cp315-musllinux_1_1_armv7l.whl", hash = "sha256:84bc765b282a9d5b7fe0348b8648904f25a6a04b2139da52b1dd30c8ac3a2c8f", size = 2449688, upload-time = "2026-10-11T18:34:23.321Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/b3/036bde636db8f76d92996e81aefc75678ab5cec4a07eea1ad0c72a893fc3/pydantic_core-2.50.1-cp315-cp315-musllinux_1_1_x86_64.whl", hash = "sha256:ed1e728b39a383c81035b2459cfcb35d99dfb01f7d6ebe3a913bc1cc5b81e459", size = 2479286, upload-time = "2026-10-11T18:34:25.214Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/a3/07f018294ee18d144afeb6df1a47d9e92960f014be45c5ed13519fa5af95/pydantic_core-2.50.1-cp315-cp315-win32.whl", hash = "sha256:bc94f474417604bd383d2cd445d071b07dd55fedceed3ce33407bf1fcc107290", size = 2103683, upload-time = "2026-10-11T18:34:27.42Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/98/f9bd7e1f9b6709f155acb9ef826d9c3884fe925f811fd8e55b9b52280bad/pydantic_core-2.50.1-cp315-cp315-win_amd64.whl", hash = "sha256:983a662de2571cb2502fc8ff47b6770b03d025d2eb314c92f77b3f07c74720ed", size = 2171681, upload-time = "2026-10-11T18:34:29.508Z" },
+ { url = "https://files.pythonhosted.org/packages/10/87/4bb3e1e7f385c076ab5af4d6dd0571b22042cd8207eb810d5b9fef15ae31/pydantic_core-2.50.1-cp315-cp315-win_arm64.whl", hash = "sha256:94845ff54dc5193f228cab81b2662a04bfbb892e95bdc15edf7399000ce57d54", size = 2156251, upload-time = "2026-10-11T18:34:31.455Z" },
+ { url = "https://files.pythonhosted.org/packages/47/47/83643225b08f2aef6c8cc4bbe6e3f79c5e139c4364a6e450d0f399d87774/pydantic_core-2.50.1-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:4a53d13cdfbedbfa87f08b83c1a0a5efcc767d785a4b41934fa9cb672670493a", size = 2234363, upload-time = "2026-10-11T18:34:33.65Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/66/127ca649ba2f2462039dc1e694c6c01e00a3689f023a617364d3507e6d97/pydantic_core-2.50.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:efbecf43d321f7b9281441f1f213f7c21c66988b0e06c2730ba13ed47a46bb08", size = 2000972, upload-time = "2026-10-11T18:34:36.037Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/4f/e421e0a5d653b1203b090b2e48745976988d7338a647940704b5b9c2b399/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc1f08f68dac9f9e83845a8039880aba2ab553eb9b2259c3243a313182c253fe", size = 2022925, upload-time = "2026-10-11T18:34:37.972Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/4e/ea5568e2491e1a71100f15ae8c2d01ef52db42a184a2d4e716bc79e5eb8f/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5dfe41f232befddb9c4377f6cfc702b51595e2d78ed082672adf8758d2c4619f", size = 2192867, upload-time = "2026-10-11T18:34:39.921Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/5e/3b8c3a35acbe219909ada5defad5d7d9fed845fb2b37bd5ec518f453c119/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:adc06d218a1cadfd2ec4628424d7d79ce4eba69c2965e7e7b55106f0da5208c8", size = 2388687, upload-time = "2026-10-11T18:34:42.184Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/aa/7889b4e515f91a2e8c0ae6b5081fec0feb30c4398d1434e14793c60f173a/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2cf91809d0721ab81592ba67bea7694821679c10b1a2e3c3460082b286c1918a", size = 2464415, upload-time = "2026-10-11T18:34:44.384Z" },
+ { url = "https://files.pythonhosted.org/packages/08/78/93449e628eb8a6fdcce3eff9043081179d1bc7ce6f1bff32dc5006f41e00/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23923ab9292c40da026330b1ecf4dc2618c8e86e0422e5d1fbf50d94d64ca4f8", size = 2150823, upload-time = "2026-10-11T18:34:46.392Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/f1/72c5bc129fceb0d00f05dc1e67f518c1728de1928c55f81fc13d7690de39/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:f3377c8c2b3ce898423c5e5dd94c7982e30aa7717a7e6ab2470b9de364963709", size = 2245806, upload-time = "2026-10-11T18:34:48.805Z" },
+ { url = "https://files.pythonhosted.org/packages/28/2a/922a0e78f3aa6ab837b59f88190233fb8dad546c17995fde9bb3ed3b9b49/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:455a773617b5913bf5c20d0692e5787b119e52c4d40ea644ca31f5758fd31be2", size = 2264160, upload-time = "2026-10-11T18:34:50.876Z" },
+ { url = "https://files.pythonhosted.org/packages/52/a8/0f1449e3e1b20941c9372faa18e7b6a092e30cdfa841902473f7989532ac/pydantic_core-2.50.1-cp315-cp315t-musllinux_1_1_aarch64.whl", hash = "sha256:1a9006395dece0e32e704c315eff8a00bede494f6108546cfc5539c89fef4f9a", size = 2294031, upload-time = "2026-10-11T18:34:52.876Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/d0/1031f492857de70355fb16524bbb03efce5fd34c93ed4a1ec60be07e0d4b/pydantic_core-2.50.1-cp315-cp315t-musllinux_1_1_armv7l.whl", hash = "sha256:d2d82aa62521c55ddfb000ae70f88cdd8de974078f6024e821dfe5addd0c818f", size = 2468423, upload-time = "2026-10-11T18:34:54.995Z" },
+ { url = "https://files.pythonhosted.org/packages/46/52/269ffffa645b8e47906395a39cf9db1151ae9fa4bcd7f47b960bc31baf37/pydantic_core-2.50.1-cp315-cp315t-musllinux_1_1_x86_64.whl", hash = "sha256:009634b83993777ddcd69cad0ffcace43dabde692109528e35f0fde91e386a8b", size = 2512598, upload-time = "2026-10-11T18:34:57.149Z" },
+ { url = "https://files.pythonhosted.org/packages/31/5c/e47e28281f20326ff6f3c31d626f0a83e615d2d94ba41bb0ad6ec237184a/pydantic_core-2.50.1-cp315-cp315t-win32.whl", hash = "sha256:3fde4fdc6487a58d944ca87cf5adc95d5f266e872c19599f5f4c0a8a1b1f9f9f", size = 2098535, upload-time = "2026-10-11T18:34:59.313Z" },
+ { url = "https://files.pythonhosted.org/packages/03/ad/759e181e69c1b472c60b2049e5f61d5da1deefdd5ce1df4bb2ebcf771f25/pydantic_core-2.50.1-cp315-cp315t-win_amd64.whl", hash = "sha256:1c8632d4ac04e6f91128fca584b3a8a507d81604c24eeaaad00d4be42765c32b", size = 2148645, upload-time = "2026-10-11T18:35:01.571Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/56/8a702c27e5be9f47e5f19d8669227424290e4c024e7c370279cbaf244b4e/pydantic_core-2.50.1-cp315-cp315t-win_arm64.whl", hash = "sha256:c3ede305158e75510be50869b319550ab072008c13d64d4ab1e094fb286b6f44", size = 2175297, upload-time = "2026-10-11T18:35:04.079Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/09/1bcf160f3cd6333e2a76982fae5963e5032de0bcaaba38464223dc266bfe/pydantic_core-2.50.1-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:76e2e83fa6ec8cdc972d438dafc2522b3a47bee4ec0ae668b29cfb1977ab5242", size = 2217070, upload-time = "2026-10-11T18:35:15.688Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/81/f02de95eec7794dfc7a5bc69a834f0e834b31b3009014ce6de903855f775/pydantic_core-2.50.1-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:a51eee75939cf811ac09b278745a6cee7dc873ccfbc8b9af3cc88fe4b7ce25b5", size = 2039027, upload-time = "2026-10-11T18:35:18.07Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/16/227746ed3771d9bf2304568b55134849ece34e13b38b1d76de763015c77f/pydantic_core-2.50.1-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae28183297fb0d2b8dc46a1f01d51f5e45825fc5afe76a835a6cb7fb34821295", size = 2109009, upload-time = "2026-10-11T18:35:20.218Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/b9/7664d1592a0e5a74903f357cd5b28ceb7336f3473256c5a4e8c8432f1881/pydantic_core-2.50.1-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88e492e8b9d0312e7dc13667c30222abf284dc3b79b5302b3607b41a5784ce61", size = 2269883, upload-time = "2026-10-11T18:35:22.48Z" },
+]
+
+[[package]]
+name = "pygments"
+version = "2.21.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" },
+]
+
+[[package]]
+name = "pytest"
+version = "9.1.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+ { name = "iniconfig" },
+ { name = "packaging" },
+ { name = "pluggy" },
+ { name = "pygments" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
+]
+
+[[package]]
+name = "pytest-asyncio"
+version = "1.4.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pytest" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/43/7c/d36d04db312ecf4298932ef77e6e4a9e8ad017906e24e34f0b0c361a2473/pytest_asyncio-1.4.0.tar.gz", hash = "sha256:c6c0d2259945122819f171a32ecea2c349ead889ee28176caaf492143424be42", size = 58514, upload-time = "2026-05-26T09:56:04.083Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/03/e2/08a497ef684b88559c9cc5f4ad53a37e7b99e727094a86d6ea32536d5d3c/pytest_asyncio-1.4.0-py3-none-any.whl", hash = "sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1", size = 16930, upload-time = "2026-05-26T09:56:02.576Z" },
+]
+
+[[package]]
+name = "python-dotenv"
+version = "1.2.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/74/26/2fbeedb218a787a5eea551c7532cac4e009f83d689dd2faa0d0353473f86/python_dotenv-1.2.4.tar.gz", hash = "sha256:f0d53e69935a851c0dcc78f3ab7aaccd8cabef0b92382b576b824212902873c0", size = 60824, upload-time = "2026-10-01T05:36:10Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/60/d1/38f3a3405989a89ac18390803e70c6ad7c7760da4f9b83cbeca0c44a0c72/python_dotenv-1.2.4-py3-none-any.whl", hash = "sha256:42269a8a5b3fd54ffa6f3d84b18abed50064717576b4ecf03dc4a55d8aa04fdc", size = 23266, upload-time = "2026-10-01T05:36:08.633Z" },
+]
+
+[[package]]
+name = "python-ulid"
+version = "4.0.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d6/41/65079023c81491a21799c0120bce5925366b6913596bf797806f19973290/python_ulid-4.0.1.tar.gz", hash = "sha256:bbeec02556190bb9dc3401faa7268696acbfbe7b6db9908c155dc3548629f20c", size = 101683, upload-time = "2026-07-20T15:21:41.256Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b4/15/8b39b36f55b6618ec4ca9b55134dcfd9c04cecbc72709f5d8e6bdebed9cd/python_ulid-4.0.1-py3-none-any.whl", hash = "sha256:6f1d69ceb97e99fe542df8476ebcd7a668284bf53ee14b3106bcc6a341a95ed9", size = 14609, upload-time = "2026-07-20T15:21:40.214Z" },
+]
+
+[[package]]
+name = "pytz"
+version = "2026.5"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/14/21/d83d6ef28c4c912c4bb4d1dcf591f7b8c6bde87b9c66f9f454677314e16d/pytz-2026.5.tar.gz", hash = "sha256:fa23724b9c486543b9ff54a327ee7569ac83ade54bb9afd0fc18676620401c86", size = 318572, upload-time = "2026-10-04T02:37:58.719Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/4f/ef/c66110d46fb800dda0bf33164182dfadabe26a90e4476844d502a23dca8e/pytz-2026.5-py2.py3-none-any.whl", hash = "sha256:e658af3757f9e26a9d25dd2aff38335acd92bc9104f890a894b2c1ba28311b03", size = 506342, upload-time = "2026-10-04T02:37:56.814Z" },
+]
+
+[[package]]
+name = "pyyaml"
+version = "6.0.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" },
+ { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" },
+ { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" },
+ { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" },
+ { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" },
+ { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" },
+ { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
+ { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
+ { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" },
+ { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" },
+ { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
+ { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
+ { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
+ { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
+ { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" },
+ { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" },
+ { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" },
+ { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
+]
+
+[[package]]
+name = "starlette"
+version = "1.8.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/e9/0c/6efb252d091ecccd7d62048ae11f0ea35cd75a4fbaeea5e30f9c3bf91d10/starlette-1.8.0.tar.gz", hash = "sha256:1565dc0b35d5737a271ed1e0e04e949f4e81198799f216d2667b0a0fb9cf9522", size = 2730457, upload-time = "2026-10-13T07:54:39.53Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c1/b0/5742e4ac7af5eb58ec3470a537a49d7aa507e5539413e504b3a65ef50ba8/starlette-1.8.0-py3-none-any.whl", hash = "sha256:dfdd6b29c26483288088d990eee59631dedadd66ce20d203402a7ca8e3c4656f", size = 79612, upload-time = "2026-10-13T07:54:38.019Z" },
+]
+
+[[package]]
+name = "typing-extensions"
+version = "4.16.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" },
+]
+
+[[package]]
+name = "typing-inspection"
+version = "0.4.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a3/26/b09b8010994eccc3c09092e6b34058f36a460eea2d4c3e8b910c695975a0/typing_inspection-0.4.4.tar.gz", hash = "sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47", size = 76928, upload-time = "2026-08-12T12:37:25.997Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/67/81/4add07e5172b7ac40d8ed5ff580409a7801a4fe26d529bdd915401dabfbe/typing_inspection-0.4.4-py3-none-any.whl", hash = "sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147", size = 14750, upload-time = "2026-08-12T12:37:24.648Z" },
+]
+
+[[package]]
+name = "uvicorn"
+version = "0.54.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "click" },
+ { name = "h11" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/da/34/30e9280707135d2cfc589dfff3cb796bd07a3aeb1a3e415ba09dd89d7bb4/uvicorn-0.54.0.tar.gz", hash = "sha256:a2e33cbfaa0306f8e6b0c13e0cb89d7d7a2da3e62b90c66e18c33d9807b28620", size = 112283, upload-time = "2026-09-25T06:52:37.601Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/38/0c/b54a4fdd7f90a3af8b02ebc9ce6712c2c208b7926a2f7bad95c33ebbe943/uvicorn-0.54.0-py3-none-any.whl", hash = "sha256:505bdb0f318731d45f1f712071fc781a8981f6847a31c902c9f5e652d4f67faf", size = 87427, upload-time = "2026-09-25T06:52:35.829Z" },
+]
+
+[package.optional-dependencies]
+standard = [
+ { name = "httptools" },
+ { name = "python-dotenv" },
+ { name = "pyyaml" },
+ { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" },
+ { name = "watchfiles" },
+ { name = "websockets" },
+]
+
+[[package]]
+name = "uvloop"
+version = "0.23.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/fa/42/02c739ce85fb2ee8d99212c61417da8140c6b87e9d97c430bea520d76044/uvloop-0.23.0.tar.gz", hash = "sha256:28d160f51ab4da3b187063652e643dea6831072add4adc1e6d62afbe73b6be27", size = 2559185, upload-time = "2026-10-01T03:17:04.4Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/05/98/04e766a6de99e6f7f955ecb7829e8d5a557de3427cb85be2236de54dda0c/uvloop-0.23.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:93935ab27b6eaef4c3e5489aebc84284f0644592f7ab516df60ee1b27eaf5eb3", size = 1393055, upload-time = "2026-10-01T03:15:42.526Z" },
+ { url = "https://files.pythonhosted.org/packages/33/8a/499e7b863a848ede009539bce39806b66205da5f8779354228e785601144/uvloop-0.23.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4448e9124537620f9c25d004c227bb5104440b58955c19bbd312d910af919a63", size = 768909, upload-time = "2026-10-01T03:15:43.974Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/95/a880f8ce3b87ac5b307c354e8ee480be4658d24bf01f87921d57e3530b4a/uvloop-0.23.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7548ede3ee908cfabc0d068106e303a9a2d811af959cdf6ab85676344cedcda", size = 4419106, upload-time = "2026-10-01T03:15:45.551Z" },
+ { url = "https://files.pythonhosted.org/packages/51/27/c1d2f9fa977f8f42ea294604166df10e0027e6dc6cd17f85ede386c9bf36/uvloop-0.23.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:090865d8ce7a03986755a3ce711b7dd0d4b44eb14ab74368b717f3fad1180208", size = 4532597, upload-time = "2026-10-01T03:15:47.258Z" },
+ { url = "https://files.pythonhosted.org/packages/42/dd/2cb6a2c8a30ca55c07a882dd4ae4ceae0fa7d8c15b25b3b7cb9a4b6cf4ca/uvloop-0.23.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bd6f2f81c7b9da99d301c0b16b82044e76fe887086e42e1590ecf520b94dbdac", size = 4230048, upload-time = "2026-10-01T03:15:49.119Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/52/29989cbaa4022dc4ef35c1dd60a4ab989e4c2065f341ed483ae71d2bd950/uvloop-0.23.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a6ac96da66c35bf789bdcde78a88dc7d56b7907d8379648c54adc1c61594575d", size = 4394152, upload-time = "2026-10-01T03:15:50.829Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/83/eb980d64e6dd5da46d4dc35755fa6afd6b5b47141437cf89615f1117c5a6/uvloop-0.23.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2dcff2d69be43e6559e5dad2c5a7a2dbfb60e05a77311b6c4b7a4a8123d86c65", size = 1412726, upload-time = "2026-10-01T03:15:52.49Z" },
+ { url = "https://files.pythonhosted.org/packages/04/c1/02a725e7698134c647904bdee6589e2be14a0e7fc9942c74f86e2b90d48b/uvloop-0.23.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:19c64108b507cd0bc140e400e3396bacebd9d504956aa7726272bf6de7d9aabb", size = 779071, upload-time = "2026-10-01T03:15:54.02Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/1d/cde53c79e8c01884ad1cdca8e407e086d523362cfe4139e2c2a8dde27304/uvloop-0.23.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1748321e3c59a14a75404b1ae8d5a8d81c4e201803ea0e14c1b6fd84421024b5", size = 4395323, upload-time = "2026-10-01T03:15:55.549Z" },
+ { url = "https://files.pythonhosted.org/packages/98/54/b12915bebbf99d7ae0796211e7f5977b95f069830dca45dc1a346d84125d/uvloop-0.23.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e2cba180d6451822763eda8364f342435a873bcfb3849cbd82fdeca248ca65eb", size = 4480449, upload-time = "2026-10-01T03:15:57.362Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/8e/da6de68c31549a052a105fc76f5a9a204f6df22cb0909440aa4dbb06f9a2/uvloop-0.23.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dc61e4f9e37b507069dc7e659ae28bca7adcb04c993c3508214315d12c63f848", size = 4219177, upload-time = "2026-10-01T03:15:59.351Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/c3/1b53c6a89dc9c9d5cb75eb9a0b891ad69b32e1421ad3aa01617a9cbdcc78/uvloop-0.23.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7337b06a9f9ed9ea3049f04b76f65819db9b19bb832ee598e97b388eadf25e5f", size = 4346132, upload-time = "2026-10-01T03:16:01.064Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/a4/00e85345871c59c834a23c136c1771205856028ecc8ba940b3951178e59b/uvloop-0.23.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:b90397a50ad6332ed3e459c648ac20d182cce24a557354363ad85fc9ea4a17cd", size = 1421363, upload-time = "2026-10-01T03:16:02.599Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/a9/e5f0f3cfde30af3ec32eba8ec07bccdba2b5116afbd1ecc53edfeb0a0790/uvloop-0.23.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:be53e1d5f83de43dc175c87612ecc128d444b38e5c56cb3f807f5a73d6887476", size = 785177, upload-time = "2026-10-01T03:16:04.018Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/79/9ddf78f8cd75a15c14a09a57f59c587b8cd9d82802c5c8368b9c3ebefa0b/uvloop-0.23.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b3cbc4f96ddfa1fb88a78a69dd851369825b7816d9702eee8c4461505ba172e", size = 4381060, upload-time = "2026-10-01T03:16:05.642Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/20/57d63c44d32326878fcad5c63854afc9deb394ed95673c1b1a429178c79d/uvloop-0.23.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:31e0cf90bc8fd88784f6802cdba968a51fb1aec1cc3feec74d862b2d371d1330", size = 4418891, upload-time = "2026-10-01T03:16:07.326Z" },
+ { url = "https://files.pythonhosted.org/packages/12/c5/0795abecda2cc3dfe41033f880a32a9ff103be4e6b177ac736833c153a0e/uvloop-0.23.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa8ed556fcc87a4091cf61587ef172fa104323dc89ecc085a618ba7ff8629a8f", size = 4214811, upload-time = "2026-10-01T03:16:09.13Z" },
+ { url = "https://files.pythonhosted.org/packages/20/18/9010dacd5221eec1bd79a4a83ac68f3db6a42d7bb657f7b640c4838ca6b6/uvloop-0.23.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f3fbfe82829d8e381426a289b87e59e585278728361db9ce975b88b51f64f410", size = 4294876, upload-time = "2026-10-01T03:16:10.875Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/08/f6384a03c771d00067cba4f542a69b2fc1a982e9fd78b357c2f788678d72/uvloop-0.23.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:7e35c9bc977760981693e1a7a51493b58ee5a501f9ebb1e547565ee40b6c6208", size = 1494811, upload-time = "2026-10-01T03:16:12.399Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/01/756a4fb24a449f313cf4a153eb0c6210b49cfe5539255ec9fb1e17d2c4ef/uvloop-0.23.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5bb9be71d9ee39b4359b832f9569518ec9bc08704194034e79e4958e6bc4d46d", size = 819396, upload-time = "2026-10-01T03:16:14.094Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/45/e314b0c600b14f53dad3a3c2d7a922a249a88225fd727652b53e1854b9dd/uvloop-0.23.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e84575f11873c109cf3962ad0bdf679094466184125f4cadcc41a73febff41f", size = 4734966, upload-time = "2026-10-01T03:16:15.815Z" },
+ { url = "https://files.pythonhosted.org/packages/66/0d/8686a7f0b1b2d55ebd770ba21f8e0e4ffa0cde5ab738f43ffb8264499052/uvloop-0.23.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bbbdb8fcd5e7062e546eec1ac78c28bb21ae7df54c18f8e4b06e15a18d661a49", size = 4584963, upload-time = "2026-10-01T03:16:18.198Z" },
+ { url = "https://files.pythonhosted.org/packages/78/b2/034a2d47e435ac02357c42956246887167bdc0357bdd6ad31c5f6d94497b/uvloop-0.23.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:76345f51367fb1f23e08605c6efb18374f669be5b223658fbab6b17627950507", size = 4421388, upload-time = "2026-10-01T03:16:19.953Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/77/131f4b583e6b4b715c404a66b51c812d701db20f25c9018b188a2b00062c/uvloop-0.23.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c7ef4701a96553514b2688e342ef1bf2beae6cfd172d89a76c768292aabf405", size = 4402414, upload-time = "2026-10-01T03:16:21.716Z" },
+ { url = "https://files.pythonhosted.org/packages/58/3d/ee11f4718ea1280595c67ed25c83d4c92115dc100bbdfd192d3ed9339168/uvloop-0.23.0-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:f1341c6abcee1c31277cfe28d34e46196f2143ec3d755e6efe7452126e1f626d", size = 1418095, upload-time = "2026-10-01T03:16:23.241Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/0c/7ca516a0671418517d79a09d3ff2ccbb44af94c75711afa6e4cf58aa6f65/uvloop-0.23.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:e095f9e105af76593b4c183bb0bcbdae64bd913a59ec595732dc108b48730ab5", size = 784837, upload-time = "2026-10-01T03:16:24.666Z" },
+ { url = "https://files.pythonhosted.org/packages/35/95/75d4e28e596d505b7ae11de517646b4ca3d369fb8537ba755410380da11a/uvloop-0.23.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f673d835bdb1a60229cc3609a113fd2c9ce3f4a3c75ad4eaed111180c00199d2", size = 4380276, upload-time = "2026-10-01T03:16:26.389Z" },
+ { url = "https://files.pythonhosted.org/packages/10/99/68daf827ad62efaf4667d1f3fda127046d42161178396bdd93aab3684082/uvloop-0.23.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c3f23f403a273900d57de6ee5ca0614c650f7f58563065dad1a4744498960e53", size = 4451496, upload-time = "2026-10-01T03:16:28.364Z" },
+ { url = "https://files.pythonhosted.org/packages/71/69/f67e696ee688f426a96f99099bae26fec14a1d0fa75dccdd6518ee267c0c/uvloop-0.23.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:cbe8d03d4efcccdb7fcedecbaa1e1fa02913eaf3a74cb933634a6bc6d2ea9e2a", size = 4212541, upload-time = "2026-10-01T03:16:30.014Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/6a/c8c436a9d7453297b4be70bdf6a9f9fc9400da45e0059ddf7b28ab63f4c7/uvloop-0.23.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:4f1798f56c6f4ba5ac11fa2869e5717926e4470d97a1dd42b4f59219d43b5027", size = 4319377, upload-time = "2026-10-01T03:16:31.705Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/2c/8fc15a03489299aab8a6212dfe0f137dc39836f915c87f7fd9d9ddd814de/uvloop-0.23.0-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:098a85e1393ef5202767b7e5fb41a32cd8bd81e6ee4af364c179801c4aa3f6d4", size = 1493428, upload-time = "2026-10-01T03:16:33.859Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/7c/05e4a210790229607f71460fcb2ed4a2c7bc72668d8a928ce577c22e38f8/uvloop-0.23.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:5a2bbad3a63007f7e9524d4903ba04fee252557c2acd86f9a3d4f91786695254", size = 818115, upload-time = "2026-10-01T03:16:35.45Z" },
+ { url = "https://files.pythonhosted.org/packages/65/14/a40b11c6c024213803b13955664a15754c72f64c873a33d986b26ec9ff5b/uvloop-0.23.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a08875543bbd4519faf30497506c9cda8a48470467ffdf967c7313c7a5981a8", size = 4734149, upload-time = "2026-10-01T03:16:37.025Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/83/f421a077712c1e87603bfec62744c3cd3a2f4b47378025db3d740df9af0d/uvloop-0.23.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:12634f15e6625f78b3f2922f91404c4d7173487eba11746764153f556e9852dc", size = 4661763, upload-time = "2026-10-01T03:16:38.719Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/62/25dcaa6b7e7b48f82ce633854ce96597ab768f9650931f4f86c572de392c/uvloop-0.23.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:378188efbb1524f2219d05246a3e1e5907217848d2882144dff59585f1b81d55", size = 4421324, upload-time = "2026-10-01T03:16:40.488Z" },
+ { url = "https://files.pythonhosted.org/packages/05/46/04628239b43dcef703af314202a3307d6060918e2d76aa86c5b1188f5551/uvloop-0.23.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:4b8e207c67d207a8608fec57e116511030af3495dc0109b8c333cf9cb412b16f", size = 4462501, upload-time = "2026-10-01T03:16:42.359Z" },
+]
+
+[[package]]
+name = "watchfiles"
+version = "1.3.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b3/68/e6aa0b77d217b31f8f486ec0cdfe5e00e6e38dc0be657e7d85819b9faf0a/watchfiles-1.3.0.tar.gz", hash = "sha256:99aee4a07847c06820765fd7b1b49ceac4f3f711ccb7d104655a33231de1c207", size = 108119, upload-time = "2026-09-21T09:08:55.664Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/68/fa/c0b840d5d8bafe640925408ce11095948c8945bb61be1e67d0ab629b872a/watchfiles-1.3.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:000b9688fc8133037a8b075c8ebf98f32844ff8964dda61e85c1db547dafc441", size = 399703, upload-time = "2026-09-21T09:08:13.518Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/8a/894799b485fe9473ad10422a0d9668e53fb78e3a2b5cc6061159572844ad/watchfiles-1.3.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:bbc1198edfdc90fda0600f825aa94150f428dfcbf8138746f55998e0e660d64c", size = 397647, upload-time = "2026-09-21T09:08:15.16Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/61/f277cd5ead05f5d37b98b41fbdd0c943ee0c04d49738f62e902cb291cf43/watchfiles-1.3.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4af9c464d410e8c44b58ddf1cbec7c3a02c1cb2c6c80df9c74659ba2fbda0ab5", size = 454994, upload-time = "2026-09-21T09:08:16.587Z" },
+ { url = "https://files.pythonhosted.org/packages/46/66/ea4c01382975e53b1ac1b0a0ea1b353416b8127fb6e04f5c1c313119aab6/watchfiles-1.3.0-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb94e40b9a0db19636b5e3fed0a9803452ecc7381df13b385272abb779ee83ca", size = 460105, upload-time = "2026-09-21T09:08:17.961Z" },
+ { url = "https://files.pythonhosted.org/packages/31/c3/1a501b597817096c9831c255f6270c124ce0e2faeb74124be24dcc93b86d/watchfiles-1.3.0-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7398930a2b76b9dc67a4e6b8bfc7baf30946892a86f417a3920c07bdd6febc30", size = 493761, upload-time = "2026-09-21T09:08:19.327Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/16/77352d05e152c7786a016e9f1decdb4507e8aa5bcf3343c72142a0413d14/watchfiles-1.3.0-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:025b108f2d5cc2799cb941f79380df3e8b22bfb589c218e801767fe78d598c95", size = 577500, upload-time = "2026-09-21T09:08:20.723Z" },
+ { url = "https://files.pythonhosted.org/packages/95/fe/81001c55b24466fe9911135e50269fd3a29f43e446627a92adfab54553b6/watchfiles-1.3.0-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:be9ef3cd403d756a304a0a08a112163c63da7ec72c8c9d083c98818291ff8f29", size = 469209, upload-time = "2026-09-21T09:08:22.346Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/cf/e3ad894ed5909a7c44b0707e89bc25d19a3d78b3a7aec846119443b6ae56/watchfiles-1.3.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b5768b49e426fd5b550b012c866db347cdf15c398ef98dc557b6e6b72fa74cd1", size = 458353, upload-time = "2026-09-21T09:08:23.7Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/f5/2f106829e6e2bc91ad74614730f6df484202b9a4d20d927dbf2f08d37d44/watchfiles-1.3.0-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:d36c72fcdc08f143d87316a4c4c3b064e340da2c7383d39803950740ed418935", size = 468707, upload-time = "2026-09-21T09:08:25.167Z" },
+ { url = "https://files.pythonhosted.org/packages/15/37/2741ab776b978669c1f3bba1e573f1a184f727ab8b4d3a6d9c61090800ed/watchfiles-1.3.0-cp310-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:f0c9865240e065a2247f4b5248528ae3d01e0e8ae071462c250c884dc16e3ee3", size = 632510, upload-time = "2026-09-21T09:08:26.532Z" },
+ { url = "https://files.pythonhosted.org/packages/48/64/db4a4684275fe9b696ee5118c194705c1423b3d42181c3f8ee70194d5c73/watchfiles-1.3.0-cp310-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:380f513d26cc2e598266b88d66456e07d67ef4be8f0f435a1154d9c44b43d509", size = 661723, upload-time = "2026-09-21T09:08:28.004Z" },
+ { url = "https://files.pythonhosted.org/packages/52/80/95ddb24f6a2f595c031c24432ab7b8aa7d6d1ada0c6d63a552166e06e8d1/watchfiles-1.3.0-cp310-abi3-win32.whl", hash = "sha256:509d9f74d2bec5c1f4cd868ddcfe0c9601f7ea53066dfcb19d8ede742c4de661", size = 277761, upload-time = "2026-09-21T09:08:29.335Z" },
+ { url = "https://files.pythonhosted.org/packages/48/a1/1c4a1b3c8030cd03206232612be8a6e8799093490c220b0fcb935d02b75a/watchfiles-1.3.0-cp310-abi3-win_amd64.whl", hash = "sha256:1acabde19b67e673274e89a04d613b7fb1f122e2c4b8ec9a581e59c149264b61", size = 290643, upload-time = "2026-09-21T09:08:30.45Z" },
+ { url = "https://files.pythonhosted.org/packages/82/e0/d2db577c632d26d08078c4390dd870b8555c4e558df25d22270f0166c71f/watchfiles-1.3.0-cp310-abi3-win_arm64.whl", hash = "sha256:d978cc1dd7ba44f5590d7de74f7a9c8be7262d9e469c2c0efb8bbfc2574321dc", size = 285510, upload-time = "2026-09-21T09:08:31.561Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/55/a122743607939429ab6a7272672f49303aa02bbd34c3f06676765a3861de/watchfiles-1.3.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:e0204f90677b7fb8d3a824279a71a01166bea9f6e53923b8056592d87b33b0b6", size = 395978, upload-time = "2026-09-21T09:08:32.747Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/9d/0ee1b0ea42acfe72afa2484f0222e2386dd09992e9034a81e63cc3cdab0a/watchfiles-1.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7c1125e99f84934a92996ae343501285617373acd66f16b59fd5654b27ad8d80", size = 387997, upload-time = "2026-09-21T09:08:33.953Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/f2/4319c6b683e8bd28465dfc53c750617a2a07f072eb2e3ab0182ccf812229/watchfiles-1.3.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a85e243c87109d9f48a2e0d10d6abdee8768cc114848b32281ec83d7f81d86ef", size = 448067, upload-time = "2026-09-21T09:08:35.104Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/71/9f37ac192a51a24de3a2d7fe9aac1e99b2b2668300674ff7a9f85d95beb1/watchfiles-1.3.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dd538c71766c59c732e2c5cedc39323c99c11897b99f93eac773438e54af206a", size = 452881, upload-time = "2026-09-21T09:08:36.238Z" },
+ { url = "https://files.pythonhosted.org/packages/34/07/1224667c32a4a96876ddd642da69733cbfe6a15897db0865aec5a882cebc/watchfiles-1.3.0-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da213822ec9082b62cadf3008e07a8c690ec7a3c89f6dc5e55a8cf07b56a1b26", size = 484802, upload-time = "2026-09-21T09:08:37.717Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/4d/7f29cd87e1f826df86fc0418d0b8f370ca0311c29c618406dc492aad9951/watchfiles-1.3.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d4593392e87669836670f87d62fc754059eaeb0e6157af44060a46558cebe5a3", size = 567522, upload-time = "2026-09-21T09:08:38.94Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/ac/3aa1f84abd4dbbfbae99a817dd931f77b439261c911189cad4c30fdfd470/watchfiles-1.3.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26b81bc515a0f03f66a69a6fea86eea54c8eca4173c46dd7222569f79dd2f977", size = 461997, upload-time = "2026-09-21T09:08:40.142Z" },
+ { url = "https://files.pythonhosted.org/packages/82/1c/27c66764d5b0dbb70a36770a32eca217eba739c28b24e8583a035087a3d7/watchfiles-1.3.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca39661934749df580d89f3dde266f2af7c6968f4c21c72ce2dc387d299e6aa7", size = 451856, upload-time = "2026-09-21T09:08:41.305Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/b3/a45827084d953c1d0dd3d75abf85dcac26a0f5d115ad2fe0b7e9e931573b/watchfiles-1.3.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:04c5500faa725d69a99b0f63750e80795e455bb951849e9c98a811b4324997c4", size = 459889, upload-time = "2026-09-21T09:08:42.58Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/3d/07740d23ce2bb23419bb24055c435a9abe5fe03efdaea1ba0720b831aab6/watchfiles-1.3.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:ec3cd4bb181a7b6329134204c2277c7144ab5f62cce9c5335d6f901ee7ce8d6a", size = 624482, upload-time = "2026-09-21T09:08:44.064Z" },
+ { url = "https://files.pythonhosted.org/packages/91/46/9540f8123eaec86df989f26d7704cad0448d9fb7271763a4f8f9fe23617d/watchfiles-1.3.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:e5871d4a7f788a9f64fb06f5793389d29d171e5c32d37c3ebfd520b017cc7694", size = 653724, upload-time = "2026-09-21T09:08:45.257Z" },
+ { url = "https://files.pythonhosted.org/packages/25/9d/b9deb52f1e4528ffe44338c5ebab95c32ed79203082da661de50288e130a/watchfiles-1.3.0-cp314-cp314t-win32.whl", hash = "sha256:81c989d7267bfb7b676cd32c83bec97914e396870983cbf493e29415037bff5e", size = 270863, upload-time = "2026-09-21T09:08:46.518Z" },
+ { url = "https://files.pythonhosted.org/packages/59/59/8b1881afccbafda06248be4185e28034dd73c80c85e4da24f2cb0c1321eb/watchfiles-1.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d754e049006e81a9be49dc78ba7802081cee426e968cbc63ee19e164fa2d0e39", size = 286879, upload-time = "2026-09-21T09:08:47.874Z" },
+ { url = "https://files.pythonhosted.org/packages/46/dd/41c12ae9745d5d72f709333287679d71cbba68ad7678fed872207bba94f4/watchfiles-1.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:a5b631db08fd3032db57ec6a92d1777cd60f88803287e4e941a4858c53aab1e0", size = 276924, upload-time = "2026-09-21T09:08:48.976Z" },
+]
+
+[[package]]
+name = "websockets"
+version = "17.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/01/89/3f825ab71c242fffb62ea8fe638741c290f62f8d7aadf8125ff897747af3/websockets-17.2.tar.gz", hash = "sha256:36c2fb94c990cc2545143b12690e2de6c16300f9dbe5b4f33fa300cf57dc8792", size = 188355, upload-time = "2026-10-03T14:56:53.5Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/bc/de/87854af9b38fe4738fd85f7f21c5b49558ae20aec898880894e435f33375/websockets-17.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:916ebdfd82e7fc68041d36b2b5f60361b9abce1e087454da15f8bd004839e090", size = 217757, upload-time = "2026-10-03T14:53:23.029Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/2e/1e80b5efa41544f626d56bd15ccb53dbfc56bf28bf80ab9cd6f82c4b1d20/websockets-17.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3621f3686397708b8eeabfd0a9d75267c1f29a7537d2fe31e65d099e71587fa4", size = 215439, upload-time = "2026-10-03T14:53:24.531Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/6e/82c78b595aee05be76a7ee78539323da1593c1848e4fef51c704c696568f/websockets-17.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a81e19710d48da88653473b6b9c366d47e99fe4f58e37ce415be47966748f31f", size = 215703, upload-time = "2026-10-03T14:53:26.226Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/c4/905ef6aa80423c03dba99e1e26fc0acf63a2a9a6a2d9e8c0e6a63caaf952/websockets-17.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f2731f9067976c8c4127212c0d2f2ada42d497d935e470419e029802365b12bb", size = 225023, upload-time = "2026-10-03T14:53:27.744Z" },
+ { url = "https://files.pythonhosted.org/packages/03/c0/a6d8be9c43e4456fb9597fdf8b5e0ce1f0a5df41503acce6d869536e4e23/websockets-17.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6627b913b8586b1c06db9516b31dd0dfbc621de3bb9312616d92a7e44f268a5b", size = 225299, upload-time = "2026-10-03T14:53:29.171Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/d4/976d34b5491258b0a86c2ce9b9aabb9fdd68919ffd7fe65999c14a502a98/websockets-17.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0198c4ec6a3406a2f7557c032967de426474c2c995c81076585e09d29a9f407b", size = 226540, upload-time = "2026-10-03T14:53:31.635Z" },
+ { url = "https://files.pythonhosted.org/packages/83/2f/c4cfd42f53c697a8ed123fd82b8f85fcd13b6360d47f9f1d1d45d6ec6627/websockets-17.2-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:88c6a42c2632ff469e84155e44f6ed92cb15ccb047bf5fcb59225ae5a12fd33d", size = 229371, upload-time = "2026-10-03T14:53:33.061Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/55/9a221b29c6232ff9282eecb2fc102402cb9e42a3479264db0e5fc4fe6835/websockets-17.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:eb0023e6cdb4b8ece0b33875188dd16104ad8c335361d396a98394f99e30ff7a", size = 227173, upload-time = "2026-10-03T14:53:34.502Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/07/125e6d010c56c253d3d2b93cabaea0f96d33898151a16b49066a594acecf/websockets-17.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c1c09d5d4646eb96bda2cfb97493bcea21a0956a981de116e6b1f4a9de07f3fd", size = 225929, upload-time = "2026-10-03T14:53:36.071Z" },
+ { url = "https://files.pythonhosted.org/packages/23/a8/aad3bd902aee84e1b261ad6ab83b405e4a564af43101b8ad1dc0293ff4f4/websockets-17.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0360c4dc13ac569cc245e0efa2f4d4b1e4733d24c47b8ab3f3747227b1356348", size = 223167, upload-time = "2026-10-03T14:53:37.528Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/f4/ec8ab9be1a5310b4fea829f088c7aa2b7a58b61d34bce1b2a9338635ff12/websockets-17.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:76693a16dead737946b651375ee3109d7db7ad9569a1c55c60aaed3ef85cfcc6", size = 225974, upload-time = "2026-10-03T14:53:38.959Z" },
+ { url = "https://files.pythonhosted.org/packages/65/45/ba6503f8257d3f98b0f07ebaad0fd099c9023eae744fd5b775416743597e/websockets-17.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:77a42cc507993ec5471b5283f7eef869239173b6000031543e3938a86d1af0fd", size = 224581, upload-time = "2026-10-03T14:53:40.496Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/45/05cca59a876c6776727d96fc7ba59e0b6f9aa496afbf13e7e04ad0b63678/websockets-17.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3bbc5543e39ee025d524077c5c15c2d67bc11c9f6676afe5b531839e24d701f6", size = 225347, upload-time = "2026-10-03T14:53:42.061Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/00/cf0e43292ae949b13f67535be84317102891d69fd1986ec2bf2ead42747b/websockets-17.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8da58558bfb0ca6ccac2419773521f1111e40654038b1afabdfc69c02cb82614", size = 226457, upload-time = "2026-10-03T14:53:43.575Z" },
+ { url = "https://files.pythonhosted.org/packages/79/0d/9a5c61a18f0cc9876d94c70ccb3daf7614a9fee56abbb37c0e64e757fb96/websockets-17.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:01420cb1cb47433e8e7075d32cb8017ad3ffed0654bd1e48c0251b865920dec3", size = 224011, upload-time = "2026-10-03T14:53:45.077Z" },
+ { url = "https://files.pythonhosted.org/packages/34/ed/991c1ab80ab2ce40e1c939fef6fa8f971c3ef3b21caf988a7a107e0ad27d/websockets-17.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:c49c9edd47d0e44d360299e2d8865e2950d2fcf1b4098782c9d7dcd070919e5a", size = 224990, upload-time = "2026-10-03T14:53:46.8Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/7a/363c835d17923e967fb66376188e67b9a261c85d826a0cd5e4dd3471221d/websockets-17.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:96f6c8d0fe21930d1f982bfce2382789d2e8d005d2ab63d21280660f95ef8fe1", size = 225265, upload-time = "2026-10-03T14:53:48.382Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/90/6c51f6d78636bd1cd6781fae8ea5ea7bf1d5b4059354f3c1f5f8de793338/websockets-17.2-cp312-cp312-win32.whl", hash = "sha256:b25659ab2d655d742701487d5591e3f98e8f8b329fc999e05e3d59691ab344a1", size = 218228, upload-time = "2026-10-03T14:53:49.867Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/2a/90008411c652dcfae34345a2169f4becd066a4ba71eebfa8dd801e0445e1/websockets-17.2-cp312-cp312-win_amd64.whl", hash = "sha256:faa763b677e96f1beccc6b4d7e8c079dfeed2f249f57a19debc321b519ee64ec", size = 218528, upload-time = "2026-10-03T14:53:51.486Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/a1/b8ad6c17f8e75ba2215422fffe0d7f0c4b690dcff1c47c0473db0d253d51/websockets-17.2-cp312-cp312-win_arm64.whl", hash = "sha256:63499fc49efe48bccc2fca40723bc7adb198866cbe159093dd979905316994b6", size = 218457, upload-time = "2026-10-03T14:53:52.938Z" },
+ { url = "https://files.pythonhosted.org/packages/54/54/a935a32dbc2e7365b1b59eb74b5ab7515456f02370fdca4c4efc3574e96f/websockets-17.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:b24b83fbb34b2d8de06cf0f0d4bd7737344ef854482a614826d4356c0c3f0c12", size = 217752, upload-time = "2026-10-03T14:53:54.59Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/95/cb8881851abe2662730e6c61cc521b4c96513fdf9103a44f169afce2eba8/websockets-17.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8a829db795e3f87053904493d184b185c8eb1f497c852f434168ec856aa6f997", size = 215436, upload-time = "2026-10-03T14:53:56.034Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/1e/621bb93f35ab7d337be98f1958294437527e2a1797089b5e734ddc5eec5f/websockets-17.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cf8811d285acc91216368df7fb55cc8c9bf6fcd90eea42429c7186c7385a12b9", size = 215690, upload-time = "2026-10-03T14:53:57.587Z" },
+ { url = "https://files.pythonhosted.org/packages/62/4a/49d0c983c082676d5d413b28e6ba5ae1d174c00268467bf78d9fe986a2d2/websockets-17.2-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:89c4898da776193577279173dcf9860487590611d7320d379435a145881b048d", size = 225080, upload-time = "2026-10-03T14:53:59.081Z" },
+ { url = "https://files.pythonhosted.org/packages/04/13/95a45eb410019772002d8f53d81396dad4120f7df39ca9962f86f5d7cd01/websockets-17.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d87091c4347daadbcc0833b65812ff38d7350c67339625d4e4a512cf38e3e8ef", size = 225361, upload-time = "2026-10-03T14:54:00.61Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/fe/0f0eda80bb441f54becdaf793eb20ee080926f8d2356388377cf262187e5/websockets-17.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1110fbfd530c447380e6e6db88b7e43ffe33d54178f5b0ff0aaa5a280301e668", size = 226602, upload-time = "2026-10-03T14:54:02.098Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/36/067fc09d8e6f154abde7c2f747c52cc442a02c5eb14816f5c39cb9f8bcc6/websockets-17.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:83abd8beab056aa77a116364811f8fc262dffbcc7abea48de0c85ccbfc6f1428", size = 228035, upload-time = "2026-10-03T14:54:03.545Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/a2/939bade7a396b4c381aebbf3941969f124d0f98d56753f81cd256f3fc4d6/websockets-17.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:876da8ca5520d65b5d0f2ca6b4e7a00d35bb90ccda35cb2ce3cda4b6c711e84a", size = 227227, upload-time = "2026-10-03T14:54:05.045Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/8a/37b1033e21709dd7fa39239ea4d9cd7f348ad5bcba94eb47253878576f8a/websockets-17.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8462395df8f224d2daa3d80db3ae4450d9d4b7243c8483ac79a82862f1599dd6", size = 225985, upload-time = "2026-10-03T14:54:06.81Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/3a/0d89539900b06d86366facb7558198046de125ab8c371d9248d6262da70d/websockets-17.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6e9a04e69456015e6ae5e0d486d995137fd435794442122b00ce5f9526ea3ba8", size = 223226, upload-time = "2026-10-03T14:54:08.583Z" },
+ { url = "https://files.pythonhosted.org/packages/31/9a/bfc5633e3d538d0a71cfbe7a5fee56c712e16c2dbd0ce17c83196a2a96a9/websockets-17.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8a2321bcb73758c44c8076509024d02c15ee484fe77ce04edea4bf4d257492cc", size = 226042, upload-time = "2026-10-03T14:54:10.254Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/1f/cbaf1786d8e3aeafe9d76951fc01139ec353b92555580336f23669382a55/websockets-17.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:8be4a87b3baca380ec3c7b1643b2dd268ac9d42c5097c0e8dc9a49342faf4774", size = 224639, upload-time = "2026-10-03T14:54:11.911Z" },
+ { url = "https://files.pythonhosted.org/packages/80/49/175faa5bd169486f835602ac0ae6303318aa65693b79cdc72c5ee53b148d/websockets-17.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:eb7b737ce8d18c8a08beb68f751572b7bf6a18093ecd1406ca1256b50592552e", size = 225407, upload-time = "2026-10-03T14:54:13.489Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/d1/3662f612456cfb2dcc128c8e596f0a55fb7b695025e2ebe8ba2abb355c3b/websockets-17.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d6605630c2808b33f362d6d08582e79821f77ed2bd3f49f9d467ea70defea06d", size = 226513, upload-time = "2026-10-03T14:54:15.046Z" },
+ { url = "https://files.pythonhosted.org/packages/73/6b/07af5177a49e30156b0922556fa93624a920a2b17d3e63bf4ad94668112c/websockets-17.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:dd9252828073fd0d69e7667af4275a1b17c18d0833b1ab7f59db272f194a6b9a", size = 224072, upload-time = "2026-10-03T14:54:16.574Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/34/d18054ff4d8314524164f8b8efec2cb17627287e099f122c28ed6fa598e0/websockets-17.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:06c7386128a9d85de4e1960114604f3031c084d2f4eee8db382637f1634cbab1", size = 225022, upload-time = "2026-10-03T14:54:18.143Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/12/75433caa3e9fa3e51d7751dc6bad24a86addf76cbfb51e52b11d037ba7fd/websockets-17.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:98f2d03df74977fd252831c997c388cd6c3f691a8a9d022b266d3cbd9849838f", size = 225303, upload-time = "2026-10-03T14:54:19.679Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/de/23e21c002aa2786ac9807c0876faa3b2576493b29ca3386287b0db46f021/websockets-17.2-cp313-cp313-win32.whl", hash = "sha256:5b43a1f7e4853ce08c3f6d3bf69799ee5b46548bfb71792a8158f7e45d66b547", size = 218219, upload-time = "2026-10-03T14:54:21.232Z" },
+ { url = "https://files.pythonhosted.org/packages/13/eb/960411c0c574535d629c16e96a2b4e5353dbe4109df8ecea859e1b5245ee/websockets-17.2-cp313-cp313-win_amd64.whl", hash = "sha256:27c7a59b5352a8f741b422820adfe89dfe47c8f2d84fb32111e76111edaa0e83", size = 218531, upload-time = "2026-10-03T14:54:23.025Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/1a/3ac07bb52378952eff1d52d04a7ee6e82ce84e3da319a52a4739cd9c78f5/websockets-17.2-cp313-cp313-win_arm64.whl", hash = "sha256:533b7c82bb1eafbeb921dfe131c9f88e55451ddc328d84bde1c9340ba72d2808", size = 218466, upload-time = "2026-10-03T14:54:24.857Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/74/6bc991a28ac983600e65de408ebd1b1413d554ed0468ae5c831bc52dded6/websockets-17.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:ecb748910e9ba4624ebe2057791df51dcbffb48c37108ab94a3c593472023c9e", size = 217791, upload-time = "2026-10-03T14:54:26.381Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/2f/158e99426be6e71d09520bae53f29294fbb614b2fc5fbf8867b1d08395a7/websockets-17.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2ab9af5cb7265899e659f079eb71691375a1025b6d5fbd3caa495dd08f70833a", size = 215486, upload-time = "2026-10-03T14:54:27.962Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/09/1abf942723c0001d9c2fca1551907dade6304517b982b0bf10bba107fa81/websockets-17.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:06e46da092bca3a52e98f0458c66b247993ce501a07cd09c858be3296511ab7d", size = 215699, upload-time = "2026-10-03T14:54:29.523Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/1d/1ade03963ef497c47e6bad79e24370827b2fe6145fa8f58070ff2b7dcbac/websockets-17.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fcce735ffd72ac4056db05325d9f0232382b74826f0196eb6a15ca903abdaa0f", size = 225081, upload-time = "2026-10-03T14:54:31.278Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/fd/47b8a0361c49da939b976a07b27a72a9f893d01dfcf4d2a28b53419ce1ef/websockets-17.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:42cbca10f82a8b2fb1536e8a0830ca6ceeb6bb3d8d64b766e0795369135654a8", size = 225430, upload-time = "2026-10-03T14:54:32.917Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/26/f4d4c76264ee037c5556ab5f50fcba302746dabf7528955534e4dda9965e/websockets-17.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c63ff5a21f26bd0e6a8464b53fadbe174825c8718ac14180df45665eaacdb6af", size = 226676, upload-time = "2026-10-03T14:54:34.833Z" },
+ { url = "https://files.pythonhosted.org/packages/37/b3/c8b1c981322a050c4babfd327ffc9880f9c3834f5b15d2574e37eeb8768c/websockets-17.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:63f543463601c1558b755f8dd7618b6ec3dd0934dda051d3b7030d8c76e54de2", size = 228048, upload-time = "2026-10-03T14:54:36.424Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/5a/1cb29ddb23e6bc27ffd1c5316cd3616360d1ba0c3854eaa134ee3207bd28/websockets-17.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4c32eb565ad9ce8a6444248e5b7a19dbb86a81c811fe5fcc2fba7a735aed5163", size = 227281, upload-time = "2026-10-03T14:54:38.01Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/64/135274572dc0c845fc1111e2b932c807c395daac75d6eae6cfa148d8a208/websockets-17.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5d459bbb6c22f26dcebea56924a362aba50d453b9867912862c970434fcf0d94", size = 226025, upload-time = "2026-10-03T14:54:39.613Z" },
+ { url = "https://files.pythonhosted.org/packages/58/75/f1e386aec3124489411caf5138cdd5a2bc43d3fd4a681c69adcf5f6272a5/websockets-17.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f19ca1a21871f024e38faf4107b433047df27558dff1b72a1dac31481e2c1fe5", size = 223277, upload-time = "2026-10-03T14:54:41.165Z" },
+ { url = "https://files.pythonhosted.org/packages/60/eb/24733a0f568c2eb99e60f9faa620a98fb228c06a01e7e2f348b33290ed9c/websockets-17.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c76b4bcbf0f713194591673fc86a42820e14da6bbd1bb445d3d002cc4d1e4521", size = 226148, upload-time = "2026-10-03T14:54:42.779Z" },
+ { url = "https://files.pythonhosted.org/packages/55/6d/ea66a30af74f5983cae31ebb9ef78b178b366a12856a414e1472225c4a34/websockets-17.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:30201a7f69833b015556c72feb69ea501b645986fd0b90dab13f589e995ff428", size = 224615, upload-time = "2026-10-03T14:54:44.41Z" },
+ { url = "https://files.pythonhosted.org/packages/87/80/c6f2228ad89774429d270179375ebddb657119215f52d1df7c680d65cad7/websockets-17.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:0c8600aec354cc259f1691b0b42816f04a9886a953f82cb227246df76057f97a", size = 225398, upload-time = "2026-10-03T14:54:46.063Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/4a/3d8da19732ad468d4be7f1e3ac298078b60bdda55edde6589bef84a5eb7e/websockets-17.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:307fc22ea496be8542d67b82ae8c867a978dfd19ac35573d4f15943fd9277dfe", size = 226571, upload-time = "2026-10-03T14:54:47.672Z" },
+ { url = "https://files.pythonhosted.org/packages/58/22/1231657122d9cc24791bb90af13cc2f4e84cf0d3a454cb37e3abfdcb2fd9/websockets-17.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:9c88697fa943bd4ef67cc919a17d81de6581846f52bfa8c6f64a916098986556", size = 224125, upload-time = "2026-10-03T14:54:49.537Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/04/350ca2445da758bc42cdb4218b44d4ce0d5a9c1d5e4cc4a58d64348ad9da/websockets-17.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f7eac84d4969da82166d5e90d9c38d2f416fe24f9708a7013569b193745b9a31", size = 225081, upload-time = "2026-10-03T14:54:51.075Z" },
+ { url = "https://files.pythonhosted.org/packages/da/c4/dec952b0df3a5d918ed2a545abb0c25ae519c3bc2d9aba3b7c46abae8f05/websockets-17.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:313f6703023d53baabab6d6c5c37cf637b2c4fee255acf2ed5e92ad69e28f1b7", size = 225376, upload-time = "2026-10-03T14:54:52.675Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/b4/198a260afbcc086ff4979774e51834ed7fb5b95f9ef305e0c4924630b857/websockets-17.2-cp314-cp314-win32.whl", hash = "sha256:08d90cf344bdb971ba3a826b78d4da9bfd56cc6a97a604d9b88cbd40bfa6c735", size = 217760, upload-time = "2026-10-03T14:54:54.247Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/9e/0523f8bc2f7aaddf39562d4fa01b4d38fa61b23d980917a16d2dd19c8dac/websockets-17.2-cp314-cp314-win_amd64.whl", hash = "sha256:dac93bf7a9beb215be3282b8441173cd50806c41c007b8be9bb24e03c60ad563", size = 218104, upload-time = "2026-10-03T14:54:55.845Z" },
+ { url = "https://files.pythonhosted.org/packages/55/17/7b8bb4cb64a199e7082f1f9be784d657842fefc327ac777d6c1493504804/websockets-17.2-cp314-cp314-win_arm64.whl", hash = "sha256:2ab742249f953d148a9ba696c8b9944361e8cb92e8bc61ba2dd53a178403afd3", size = 217989, upload-time = "2026-10-03T14:54:57.376Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/76/f54ed054b6e860f1e0bbc7019542a048352d41231fdff6d904b379f881c7/websockets-17.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a69ce25be5f1330ee1c74eb6fabbbceaa96b384beedd2627cecded7546490c40", size = 218125, upload-time = "2026-10-03T14:54:58.943Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/4c/0f3375cea66a125ae01d21fb9c537aae955ef499bfe7e2b2376a34362f2a/websockets-17.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8e24b878cf54843a63985d90480f163ca7f692689fbcbe9cdbd8165521083a8b", size = 215658, upload-time = "2026-10-03T14:55:00.674Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/05/7c871a67bfb4b61adc1fe13583db97803f87dfeca644fe6ef51df7bb276d/websockets-17.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f33c7908a6885dcae9f462a4a8347b637053b4ff2b96beb4c23fba1cf7818e5f", size = 215858, upload-time = "2026-10-03T14:55:02.379Z" },
+ { url = "https://files.pythonhosted.org/packages/41/8e/59df4d9cd357e902d1c74b13c3c0c3841c8df6e4b1b3d131bf26a23fdcb1/websockets-17.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c796a1bb3e4015249639849f30e8e680df8a431b45d417ba8acf843d2451d95f", size = 225443, upload-time = "2026-10-03T14:55:03.966Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/64/5e486a3a44e041203c62eccf1fc89c7f8824e21104a7b82b182e5b21c228/websockets-17.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:983bcdc898662f6ba9d6a025c30d29946ff0986d9ad60d400af0da3671f7cbf3", size = 225726, upload-time = "2026-10-03T14:55:05.797Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/98/b6eb53121c91fbe8b6897aba06861ce60f9ab58faffc6bca5750cbc21681/websockets-17.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:35e0f088ddfd9d9bc5019e27ff3767411779e92b59db5bb1507f2731a5b61158", size = 226895, upload-time = "2026-10-03T14:55:07.626Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/18/8c091321b99c91eb3eaec9acbd940e69308b4e465b5605c430af0cf7d3a5/websockets-17.2-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:19e2511412ad3393191de652513bc7a0ca3c93af143b32d96d46e59fbbddf1d4", size = 229040, upload-time = "2026-10-03T14:55:09.321Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/96/3a92f944305b7de42fcb7530b9fa69607b4b4ce993c36a9f2330dbc318ba/websockets-17.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb5e2bf969ac99a6ae3c71208a5eb05cfde973192540ffa6e1068b57fb78c4f8", size = 227469, upload-time = "2026-10-03T14:55:10.935Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/a9/624f6d75ba326c22d03698b34c0ada984f1d76196322a62f6c22903b831d/websockets-17.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:691780fca2be3dec512cb603cb91060271968cb4af86b51d07c57445c5754a37", size = 226202, upload-time = "2026-10-03T14:55:12.536Z" },
+ { url = "https://files.pythonhosted.org/packages/47/af/1e6e8c625aeb268830af2c4227fe05e8db59f4f4debe1dadfd0ada214895/websockets-17.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2d39c19b1ba6a6791050383fd69efdd3b63533e2254693d0263879cd5f5921ba", size = 223743, upload-time = "2026-10-03T14:55:14.164Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/81/33c5280f4f6f81637c93ae065c6a594dfe35935622af135a5f7c3768bf22/websockets-17.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e48ac2b302986c6f55cf61e8e36b4dd97d0132c5078a713a697a940934ba422e", size = 226492, upload-time = "2026-10-03T14:55:15.796Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/f3/7aa9fc36e67caccbcfee2c48f4ada41e9da512d41523c024d039f0f22ba3/websockets-17.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:e136197f1262620ef2e507afc3ea759c1ae7d221886da20eec5f4c9f2618c2aa", size = 224940, upload-time = "2026-10-03T14:55:17.661Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/8c/457aff7081a63d1261608bb4d7b0b0f9dfe780697a2a334671745742850b/websockets-17.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3eb44019a2b0b3b91bac95998f1e4e5589730421170e060fe654a2b7be727dc7", size = 225835, upload-time = "2026-10-03T14:55:19.607Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/c3/7a13a3b3050db2c36772ded49f8d48f99eb080948e9f6f762e7529925ab5/websockets-17.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e5855e574804398859c5fbaf4fc7882b96278b7f6572a3d889627e6eb6cfca59", size = 226848, upload-time = "2026-10-03T14:55:21.274Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/3e/d5b2c1e473b1031a4a0ec0e10de69df5b981ab4a10aa482bb45c18dd43f5/websockets-17.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5dc29815520c329f5662f6eb3ebadecf0d4f8c82dfa416d4d6efbf8f39245559", size = 224541, upload-time = "2026-10-03T14:55:22.874Z" },
+ { url = "https://files.pythonhosted.org/packages/79/5d/bb81976cc1aa546afb51395ce42913521e9dea062bb34a61308cfff30726/websockets-17.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d1a4f9462da6496b6cb79bbb09c60d17f7e63e8a1df136797b3afabec9560e4d", size = 225315, upload-time = "2026-10-03T14:55:24.443Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/6b/314962d5440c61b4c107914599c13ceeecc6bdb6e2e73a5f7e566a7d1f26/websockets-17.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9496bff5541086478264678bac73c0a75b2fde94fdf6568893bca1f7c6d50d18", size = 225747, upload-time = "2026-10-03T14:55:26.033Z" },
+ { url = "https://files.pythonhosted.org/packages/98/fc/9eb64b34a3a4458eb08f3f24bde01508f72a00790330723c158ebb965048/websockets-17.2-cp314-cp314t-win32.whl", hash = "sha256:e1e3bc8090a7eae79fdf634b63bdbfa3c93999991023c37c6fd3b469fc8ff5dc", size = 217891, upload-time = "2026-10-03T14:55:27.681Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/ed/3a4e2a09b0822d6e525cbc6e44a4885669bad5b22ab9c64fa2444bc15325/websockets-17.2-cp314-cp314t-win_amd64.whl", hash = "sha256:65a89a5bde227bfe908016f35b5bd347970cd1e5b0360f389502eba1c7fde6e0", size = 218229, upload-time = "2026-10-03T14:55:29.314Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/66/cffb75ee746dd060984c3c3e2eac7f875a866225a30dfa53e2cd18232565/websockets-17.2-cp314-cp314t-win_arm64.whl", hash = "sha256:1c27339934109dfaca83f18ab2c23db06714e9d5deca2c8e37e8f492ab90d20b", size = 218146, upload-time = "2026-10-03T14:55:31.001Z" },
+ { url = "https://files.pythonhosted.org/packages/12/e9/10a9b1633b63594054c87b97af048628cea2b21b5089a52a9fc1e0af60a3/websockets-17.2-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:a7c4bb26de6ef496d24822aee4f6a305d97cd33d21a2b85f290292d69ba1c25e", size = 217719, upload-time = "2026-10-03T14:55:32.674Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/00/ff4020fe0886dac7199a16ce2805c7afd7b981bd2e81d3fa18dff5d9863a/websockets-17.2-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:c08da1f15040bd1e1a6074bd4518a6ef20e67b1594ecfb0aa75e5b45f87e6d6d", size = 215448, upload-time = "2026-10-03T14:55:34.338Z" },
+ { url = "https://files.pythonhosted.org/packages/66/06/bc7b944f81514378b2c2ab96c17df19e871cd33b9be0f1f6dfc975457e5e/websockets-17.2-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:3117abfd32b183bdb6194df9317766d32c6517f3d1c0aa8c62d5c6ccfda0b4a8", size = 215674, upload-time = "2026-10-03T14:55:35.918Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/da/2b2b76faa2f10c4813e3872c9577fd13a798f5918b1785b86ff7d635eb2a/websockets-17.2-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a046227daa7f191e843d26b911c1146233e9a33d249e0c954dcb3ac7c398710e", size = 225119, upload-time = "2026-10-03T14:55:37.777Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/d4/22cbe288c0d5cef7620503be92c0098d82220353fc7e188034a19c517240/websockets-17.2-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2901bdf24f20bc884124b3e88c61f7ece260c20c81e610f2196007395264a4aa", size = 225549, upload-time = "2026-10-03T14:55:39.364Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/0a/504b0d3063679f2c60430c3539482d42a4cb8bd1a76646baf742030a93cc/websockets-17.2-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f60e39adfecf998488166aca8ff24ab1ac406c9ecbecbcf9b3bcfc43cb1ec9a1", size = 226717, upload-time = "2026-10-03T14:55:40.942Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/ea/5da9309cc55c2665a6eebc22c369d9918c0d77258c61e92058e6b08d5ff1/websockets-17.2-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d4df62fd8448a85c752bbea1803cb3a2785e6fc8352009ab64ad7447af079b3c", size = 228413, upload-time = "2026-10-03T14:55:42.54Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/74/5a24df72aa5500f311105687af864c27f1f9da910e968e97818c6149e6b0/websockets-17.2-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c8eea55fdfa9ba65c6981eea38bd20c800bce2f092a2803d82de764ecf0f071a", size = 227196, upload-time = "2026-10-03T14:55:44.251Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/ee/ca32cc1ed892dc4ac30a922e8f648048233fbdb8b0bce7048860ec4c60ec/websockets-17.2-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3f0def1279644acaa9bc861d4234af3f82ea9cee7e460dffac5cb63e691501e9", size = 226092, upload-time = "2026-10-03T14:55:45.842Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/0c/12d4a73324aa9798d5165d20c088f9dba66c75c871960e5d921ec66694e4/websockets-17.2-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fb78fb4158c12f77a934a003006784108a27a6553cfc0c6f10483c9c02e94f48", size = 223486, upload-time = "2026-10-03T14:55:47.45Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/a4/7fe15da5abb8f0f61e6a357593f7f2ed55724825b7db0ffe72b5c5fad68d/websockets-17.2-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:f8969ad228115ad8869b5fed801f899e52ab8ad376fdb165ba4760a277c8258a", size = 226200, upload-time = "2026-10-03T14:55:49.126Z" },
+ { url = "https://files.pythonhosted.org/packages/08/b9/4cd3a311f96a2eea0ed458bc01fe2cce42f9cd50aa9e64315dfc855d63a9/websockets-17.2-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:4a49ca342efc0800e6ae94ed5c9cbdcb319308f75e73c21181e4c24d6710e8dd", size = 224862, upload-time = "2026-10-03T14:55:50.674Z" },
+ { url = "https://files.pythonhosted.org/packages/41/b5/22caa3460f75e42bfcc74028870b556d22847ea9a9034aa03986f07f16a9/websockets-17.2-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:06fa3ce9c3154826c33d4395b225b2994aa64f1f3bcd8be8ed932019175d9268", size = 225391, upload-time = "2026-10-03T14:55:52.393Z" },
+ { url = "https://files.pythonhosted.org/packages/95/be/8d28f92092076abf1ddfb3206b0ce956120a22e7c3105f6a3029d727deae/websockets-17.2-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:50644d8715be7e0ec0682f9d7744b63008e199c5e1618a48fa153756a332235f", size = 226545, upload-time = "2026-10-03T14:55:54.127Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/7b/ff943fa383e540fe17f066cc10a3eeedef26e50fd45aae2bdc6746d6f95a/websockets-17.2-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:60deca33e584c09e91f70f8b55a0b1de7d671d6a63f051d154920f48bed717c7", size = 224352, upload-time = "2026-10-03T14:55:55.856Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/df/1e6c3e06c473c9fd833a5c1620b15e2c3b37647b91b7d41871d20bc098de/websockets-17.2-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:b5f79366a8d8dbb981d53ba800bb54a95454595ab8a4548c2b95501b32a08326", size = 225255, upload-time = "2026-10-03T14:55:57.497Z" },
+ { url = "https://files.pythonhosted.org/packages/db/f8/d8a4f988f7cbb568d8bd69da4632c5b6010aa9cd9366f285e23b73b678d9/websockets-17.2-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:f2bbf3f28d0b63157577c8b774b9136f076afa6797e1a52a2ecd477f23cad3a8", size = 225513, upload-time = "2026-10-03T14:55:59.338Z" },
+ { url = "https://files.pythonhosted.org/packages/75/e0/920357165b2797a2530fc9e271d79a9b5fee2b750b154c990c740f767af3/websockets-17.2-cp315-cp315-win32.whl", hash = "sha256:74836317b7010b579522bb52426f1e225608b042c9e78cbe2493522bebb8a318", size = 217722, upload-time = "2026-10-03T14:56:01.307Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/eb/25bdca25bbc329ffb330ef33993397d6556a871e40a0d196e757699ea3f7/websockets-17.2-cp315-cp315-win_amd64.whl", hash = "sha256:aaead3d926e9ab4124ada727d20cd62d396649917822df4f771d1f07f1079b40", size = 218017, upload-time = "2026-10-03T14:56:02.914Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/cb/ea30a552bbcd1c75f0d14bfce6c884ee36187030b85b74a242aacc02406e/websockets-17.2-cp315-cp315-win_arm64.whl", hash = "sha256:40960554e60eb60c3eec4ff9e42a80f84f8cd3ca9bc80a5481a61f1e64d807c9", size = 217929, upload-time = "2026-10-03T14:56:04.604Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/01/477664c619af8aa3c908d482e2a95e13ceed9d78f21d15902013c3bc6c28/websockets-17.2-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:9a2a60a7f0ea5f239efb6391d2b28630a640d82dad63e3bee47cf2c623c4495d", size = 218029, upload-time = "2026-10-03T14:56:06.336Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/a9/b0be62ff1c0e2bc966da56b36d3d820c7e2ad3c0c4a4ac414fc7335b214f/websockets-17.2-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:cca2fcb72c007103740fa4fc3df19fdb1a318c641c69f3b0cc47ed63a889336e", size = 215607, upload-time = "2026-10-03T14:56:08.035Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/2b/a6738530de0437a31c1b168e4096ecf790aafaf561f33a009886c7d8042e/websockets-17.2-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:b789356bc4e2e6c20ba52817f92c3fed74e24657654237ecd536c54843b80c6c", size = 215817, upload-time = "2026-10-03T14:56:09.852Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/c2/2fc44ddc419cbb09ee1708af3e78d8a4b018db01fc7e4f91bd730e2f8d9e/websockets-17.2-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:222fb626fa15701a850eccc778be17312142b2f6a0e16aea80770b7459adb784", size = 225979, upload-time = "2026-10-03T14:56:11.85Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/91/a215b14caa7ea65bc36db81609108899c259503300d1560dae9c70a135e7/websockets-17.2-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4497e87c34a2d21cbec1227858fec3af8e514dd70c47625557a122fcebc081dc", size = 226250, upload-time = "2026-10-03T14:56:13.548Z" },
+ { url = "https://files.pythonhosted.org/packages/65/b9/9406a18e9edf558ed504d2a7679371d0f8107e4ef526c80b154ea4ec9752/websockets-17.2-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6281c171557ce0e408e19d9a223f22d915117ac38a5a7f32ed83809e7492316c", size = 227579, upload-time = "2026-10-03T14:56:15.143Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/45/a73af119244f46f5130005d7ab63f1c75890c890141a0ca2adc9d97d4671/websockets-17.2-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:08d97098644728bd1895caa7ecf3090b8e563d70809870d2adb33a107bd061d0", size = 229205, upload-time = "2026-10-03T14:56:17.086Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/92/ccd8e2e921d134a56f1ed4642d276500d9e33b3dc4d6deb63d614b3e53a6/websockets-17.2-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1fdb8d5a1660307dc6d36d0b7fc725213cbd7f80800904dc4896aa3208b89121", size = 228011, upload-time = "2026-10-03T14:56:18.716Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/ef/7d71105d19a7aaab5ff87b9c712f6c1dda44e72ea56aa0e7b777f2fc274b/websockets-17.2-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:18b0a46e5e9b315e2b54ce8c3bafdeef0e1388ca363114fa868e6aab2dc58512", size = 226892, upload-time = "2026-10-03T14:56:20.412Z" },
+ { url = "https://files.pythonhosted.org/packages/56/f7/87012d628b21e66e699440f39bfa7cc55fae7f52b2c532ab62184a589624/websockets-17.2-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7f115d5d804a2163dd89245710049078b0e726a58c1f44a1f86c2c6e79055d76", size = 224241, upload-time = "2026-10-03T14:56:22.257Z" },
+ { url = "https://files.pythonhosted.org/packages/55/f5/495371068b27ee5f7c435187f9dafd62402f195e2c76063bdd4653da1565/websockets-17.2-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:1d829946a2e7630f92f9d7b45b62f3abe9f393cc2dea6a35edb3988f865e75f2", size = 227076, upload-time = "2026-10-03T14:56:23.909Z" },
+ { url = "https://files.pythonhosted.org/packages/18/18/3dce3cc6099be5e044e0fd5d0e0c9931c8e3387511cdec8014a345f619e5/websockets-17.2-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:6c274fc1572edf7c197094a0eb1887d45fdc95254bc80597dc7599550486c06a", size = 225727, upload-time = "2026-10-03T14:56:25.689Z" },
+ { url = "https://files.pythonhosted.org/packages/47/30/57d0c7aaf8d4473926fa8829b8136483f561388d1e747ae71c9f2a83d5fd/websockets-17.2-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:4173a4b8a025ae44313d9d9b4ecf31e886c7b7faf45386d51a8ca4ff2dcf3f2a", size = 226225, upload-time = "2026-10-03T14:56:27.246Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/9f/9dce1203756756c00b407b9a6b13a7500fcd38f2634d4daa3f65575814ec/websockets-17.2-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:d8cfe9522ad69b6abb26b413ed1deca43cb915cefc588433d557cb3ae1c783e2", size = 227333, upload-time = "2026-10-03T14:56:28.811Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/2f/d3b6b876678ebb03017b7afd7111fe44d54b93f036a80ebb4b481dd1ab74/websockets-17.2-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:908d81d88bb16141613a6275059b5114656d5c2f0b5400b421d54fe6f1943507", size = 225082, upload-time = "2026-10-03T14:56:30.578Z" },
+ { url = "https://files.pythonhosted.org/packages/32/b0/a69b573a5e56d2e7a5dcbb447466f442380cf81515e1cb1220cd626c8042/websockets-17.2-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:c6590e1eb624ff6b15b872421bc9a10bc6d2057635d69c6cd244ac3f928f85c6", size = 225945, upload-time = "2026-10-03T14:56:32.32Z" },
+ { url = "https://files.pythonhosted.org/packages/70/be/a72911dc8e33f74c196012366ce4d99b1a803894a377a1ed0c8e66df9caa/websockets-17.2-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:61040f6f7da5a279d2f77496c69d51132aba75f701c52bded400d4c639277b18", size = 226241, upload-time = "2026-10-03T14:56:34.142Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/a9/02a68c1d8e5572918e0962d3aad881078f73ede43abd9b1336e4efaa8909/websockets-17.2-cp315-cp315t-win32.whl", hash = "sha256:f90bad2839c185a1edf8ee22a257cfc8a39e0e337a0490ab185dfa76ef04d1bd", size = 217847, upload-time = "2026-10-03T14:56:36.204Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/bf/3d7c33b8d5e7712a60e0149c017ed50394ec5e8cf72e5cb6a1ffaf11a42d/websockets-17.2-cp315-cp315t-win_amd64.whl", hash = "sha256:315551f4ccedbbf9fd4f7e8bf037a5948c976ade0e919ba5d8f581d465f6f725", size = 218169, upload-time = "2026-10-03T14:56:37.79Z" },
+ { url = "https://files.pythonhosted.org/packages/27/57/ab34cc6460c5322e6932750fa5c6c64be89e6ee4e2707d13c4e9d3312b25/websockets-17.2-cp315-cp315t-win_arm64.whl", hash = "sha256:0a6220bdf8d5f11af71251a599092d89ac1d6bfac691c7f5951c5b07953947a0", size = 218089, upload-time = "2026-10-03T14:56:39.427Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/58/835cd51934d6780fa586f275b5d9901eead6d81569b4343b3767cdbaae4c/websockets-17.2-py3-none-any.whl", hash = "sha256:6aa59f0ef92e796b2db6f5f26550c4713c0e4036899fadf02f55e2ed4db0b7ae", size = 211883, upload-time = "2026-10-03T14:56:51.898Z" },
+]
diff --git a/projects/mila/AGENTS.md b/projects/mila/AGENTS.md
new file mode 100644
index 0000000..a1ff2e3
--- /dev/null
+++ b/projects/mila/AGENTS.md
@@ -0,0 +1,9 @@
+# AGENTS.md — Mila Tracking App
+
+## Development
+```bash
+cd projects/mila
+npm install
+npm run dev
+npm run build
+```
diff --git a/projects/mila/Dockerfile b/projects/mila/Dockerfile
new file mode 100644
index 0000000..e0562cb
--- /dev/null
+++ b/projects/mila/Dockerfile
@@ -0,0 +1,13 @@
+FROM node:20-alpine AS build
+WORKDIR /app
+COPY package*.json ./
+RUN npm install
+COPY . .
+RUN npm run build
+
+FROM nginx:alpine
+COPY nginx.conf /etc/nginx/conf.d/default.conf
+COPY --from=build /app/dist/ /usr/share/nginx/html/
+RUN mkdir -p /usr/share/nginx/html/tracking && ln -s /usr/share/nginx/html /usr/share/nginx/html/tracking/mila
+EXPOSE 80
+CMD ["nginx", "-g", "daemon off;"]
diff --git a/projects/mila/README.md b/projects/mila/README.md
new file mode 100644
index 0000000..0d1700f
--- /dev/null
+++ b/projects/mila/README.md
@@ -0,0 +1,13 @@
+# Mila Tracking App
+
+Mobile-first PWA dashboard for baby sleep, feeding, diaper changes, and WHO growth milestones.
+Built with Vue 3 (Composition API), Vite, TypeScript, and Tailwind CSS.
+
+## Features
+- **Live State & Timers**: Active sleep duration or wake window updated in real time.
+- **Sleep & Routine Tracking**: Today's naps and night sleep with running indicators.
+- **Nutritional Logging**: Bottles (ml, Pre/Muttermilch) and solids (Brei g, kcal).
+- **Diapers**: Instant 1-tap logging for wet and dirty changes.
+- **WHO Growth Standards**: Automatic LMS percentile and z-score calculations (Girls 0-24 Months).
+- **Git Sync Awareness**: Real-time warning banner if git background sync reports merge conflicts.
+- **Backend**: Backed by FastAPI DuckDB backend at `/duckdb-api/v1`.
diff --git a/projects/mila/index.html b/projects/mila/index.html
new file mode 100644
index 0000000..4ab35cd
--- /dev/null
+++ b/projects/mila/index.html
@@ -0,0 +1,37 @@
+
+
+
+
+
+ Mila — Schlaf & Entwicklung
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/projects/mila/nginx.conf b/projects/mila/nginx.conf
new file mode 100644
index 0000000..8a38046
--- /dev/null
+++ b/projects/mila/nginx.conf
@@ -0,0 +1,57 @@
+server {
+ listen 80;
+ server_name _;
+ absolute_redirect off;
+ port_in_redirect off;
+
+ root /usr/share/nginx/html;
+ index index.html;
+
+ include /etc/nginx/mime.types;
+ types {
+ application/manifest+json webmanifest;
+ }
+
+ location = / {
+ return 301 /tracking/mila/;
+ }
+
+ location = /tracking/mila {
+ return 301 /tracking/mila/;
+ }
+
+ location /tracking/mila/ {
+ try_files $uri $uri/ /tracking/mila/index.html /index.html;
+ }
+
+ location / {
+ try_files $uri $uri/ /index.html;
+ }
+
+ # Service worker
+ location ~* /sw\.js$ {
+ default_type application/javascript;
+ expires -1;
+ add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
+ add_header Service-Worker-Allowed "/tracking/mila/";
+ }
+
+ # Webmanifest and manifest.json
+ location ~* /(?:mila\.webmanifest|manifest\.webmanifest|manifest\.json)$ {
+ default_type application/manifest+json;
+ expires -1;
+ add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
+ }
+
+ # Static assets cache
+ location ~* \.(?:css|js|jpg|jpeg|gif|png|ico|svg|woff|woff2|ttf)$ {
+ expires 1y;
+ add_header Cache-Control "public";
+ }
+
+ error_page 500 502 503 504 /50x.html;
+ location = /50x.html {
+ root /usr/share/nginx/html;
+ }
+}
+
diff --git a/projects/mila/package-lock.json b/projects/mila/package-lock.json
new file mode 100644
index 0000000..a980258
--- /dev/null
+++ b/projects/mila/package-lock.json
@@ -0,0 +1,2251 @@
+{
+ "name": "mila-tracking",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "mila-tracking",
+ "version": "1.0.0",
+ "dependencies": {
+ "lucide-vue-next": "^0.468.0",
+ "vue": "^3.5.13"
+ },
+ "devDependencies": {
+ "@tailwindcss/vite": "^4.0.9",
+ "@types/node": "^22.10.2",
+ "@vitejs/plugin-vue": "^5.2.1",
+ "tailwindcss": "^4.0.9",
+ "typescript": "^5.7.2",
+ "vite": "^6.2.0",
+ "vue-tsc": "^2.2.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
+ "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
+ "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.9",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.9.tgz",
+ "integrity": "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.8"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.8",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz",
+ "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.29.7",
+ "@babel/helper-validator-identifier": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
+ "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
+ "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
+ "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
+ "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
+ "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
+ "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
+ "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
+ "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
+ "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
+ "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
+ "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
+ "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
+ "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
+ "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
+ "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
+ "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
+ "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
+ "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
+ "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
+ "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
+ "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz",
+ "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==",
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@napi-rs/lzma-linux-x64-gnu": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz",
+ "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^22.20 || ^24.12 || >=25"
+ }
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.64.6",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.64.6.tgz",
+ "integrity": "sha512-FkO19Fowv/OQeJZPVfdetJaI3/VpbZmBsnmAlR/rQ+b3QwfiGQiOpVegpSRH331kelvHTVw9hXYG3DwjAaRzyQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.64.6",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.64.6.tgz",
+ "integrity": "sha512-si5/Etm6MphPc2xYHnk455Shls/hoMjTy3mhXge4OPmvCw7TwC4KP4fKokAlKLV4XaQvLvgSHfafPcblLMYYdQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.64.6",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.64.6.tgz",
+ "integrity": "sha512-PQCl3q7b0BgvVt72i2xrWM6I5KliwwVe4GpWW5QDGyUQawDS9aPZss/aGyFqBmxdRxlE8wDc9CaxiQ5DHRSp/Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.64.6",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.64.6.tgz",
+ "integrity": "sha512-l9DFl1Itb1YYdSrP1K3U8ih44AOHa/p+Q4WMMDfGvH5qVWp+ajfgS1DlYo4yCRh8wJ2ZJv0Fqccf+ULAmMFjmg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.64.6",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.64.6.tgz",
+ "integrity": "sha512-bR4GR3205N9+od0+l5Dd9/iZTDpJkIe6d6Ydp2qbcQZJLmKBdYQQRp8gbWpbMt92twKJY66o1vK4NkoKQODIrA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.64.6",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.64.6.tgz",
+ "integrity": "sha512-XTVNaDzcFl2TeBglvxdGgsCv9JGtAGRAoJENmGBR/XvDH2mA2K0IXc2aMfP0dR8mexSXSKifJ85FixoXX1wfTw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.64.6",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.64.6.tgz",
+ "integrity": "sha512-qWc6qbc+25ugG9vqPR9P422KJBbVfZ05mjcMggoq8tsfWP+eRfC+/tiDdIlRF5VL2pDVZWN9zAhWsmu7R1+7Dw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.64.6",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.64.6.tgz",
+ "integrity": "sha512-V4Nwkp8844X/Xnww3PI1LYS0YPei8XQEVG70R+5MPZGO7AhYoun4hCN7Svs4U3wbQLZbwiz+9xRFhUZZdRl8Ww==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.64.6",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.64.6.tgz",
+ "integrity": "sha512-xb72A1aPaaIsp+a88VtRae/ovIEXislbUMyp7b9AQQU7ZkP2m/+ik6DW9TQcJyw5R8/RIailF9zExYwkjR++UA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.64.6",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.64.6.tgz",
+ "integrity": "sha512-pbcs1BVBwC43hZkjhjlF8Rx8m5G0KnhLZs9Azy/gqccP6UsNSzJr0hKGIAVPqzAH1O/NdPxxC9GVGb4msQg/aw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.64.6",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.64.6.tgz",
+ "integrity": "sha512-EKOC8os6Vsg487ZLKUqzLF99Jsmnq6tu7O7mAELakNpcA+8KadktFf/JYdRuHe/DzCUXmN38pUKu4XBcKlqGbQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.64.6",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.64.6.tgz",
+ "integrity": "sha512-v/SQ68GnSlr/HyjB79sNXesGDwkOPu9JjbHfmWaymCiM+nZsQ9TstG1KfA4MuEvTr2d3PoY8PhFxj9bt0e32JA==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.64.6",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.64.6.tgz",
+ "integrity": "sha512-G3pmliMQ7r15zFFNwBlaT/xTdSLi4lfgI7CUrQueyKYheNVhrd8JrWCE6Ll8xf4AA1RZFqq+mNJYntiElsVOyA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.64.6",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.64.6.tgz",
+ "integrity": "sha512-XQTB/IPoB/wG6x/wu9uomAt8YH61IAr5iZTubMiad/lu88sotq2ocz9hJd556+Jz2kIkF7ZQZIsUuWE3OWqtcA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.64.6",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.64.6.tgz",
+ "integrity": "sha512-O5mpokth6+ydMuwiGtY8fIjvOBfH7u6SzmDNmZ3k6GK5C6iEAC2q2ZN/T5L9jYILx0IcCT0KnfMDihQ5yQFSBw==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.64.6",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.64.6.tgz",
+ "integrity": "sha512-aIFxbAD9v3TvIaEDsKmukjcDg1JS4/tFXw8SHyQQgzOwzWCQ4cyDK7Ju1pQKqUXDFkEu/BZCFDCDATsKhQcB/Q==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.64.6",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.64.6.tgz",
+ "integrity": "sha512-fuNo1UwBc6BxnHDOxxguQL/InhheMN3RJcJMb0b4xP8r5Q3CNCrJbVg0eCIFuJ4e/zAfjR2gYbOfbzO/G7CIyw==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.64.6",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.64.6.tgz",
+ "integrity": "sha512-05v6wFKDKJPZs9xlytVrrRu6IbRuoSvlH9xNwEOafXsKyjBFZPvAcZz2VIAB/M+lblYeKa9iOYC4RDVdfcAAaw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.64.6",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.64.6.tgz",
+ "integrity": "sha512-wJXKcdMjvkc/FaavZXNP1yW4i0pfOmhZAIWRSLG2yJ0PB0TWi7dkLErhX0u0vptDcXKKO/m1Y/YTeuyvb2MXwg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.64.6",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.64.6.tgz",
+ "integrity": "sha512-cwg7/h0ThvJwy8CBkmACdiHAxRWpbsCmIsjOt+Zzx0hTnIcSUmKYEV03bGg+hD827OnwUsbKh1ZZDW754xWyBA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.64.6",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.64.6.tgz",
+ "integrity": "sha512-T0RkYIJbdYXJEQFw+tMBO09KDFdgmNxkHsof3wWAABrvnx15Yh8RytmrgYy9+q9cBeBNZF66MMQZC+5nzEz6vg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.64.6",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.64.6.tgz",
+ "integrity": "sha512-KlfpmF1AAIdAWxt8QQNClbpLFlAUfYOzqIuJ7IXEStMHpFRuH78RG6FjqPb3Rqhh0K89F5npvG+/i3Fjl6IveQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.64.6",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.64.6.tgz",
+ "integrity": "sha512-MrTLjd7Jlt9/ZavJZYG61KQv9Iy9bzbY+yXy73Xvwoa2uP66wvDShkyBr5Chz/yrE6lbD/zHMuTmKQVNQqBApg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.64.6",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.64.6.tgz",
+ "integrity": "sha512-8fWDH1CkPI8GdBKAxzHDza7UmgficsKo2+MVbvUgOGhB0629w5sHMg3Qu5y7TZsrYKHHWpz8fla4KvoT7WsO3Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.64.6",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.64.6.tgz",
+ "integrity": "sha512-9/c9PWdr5GCIHkkxiHM1HjNBQMVdftP4O8QTcoUAaRza2FSqJ4axbf8QAnBuE6BIGQDl71SaRPeDdDYid5ecTw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@tailwindcss/node": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz",
+ "integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/remapping": "^2.3.5",
+ "enhanced-resolve": "^5.24.1",
+ "jiti": "^2.7.0",
+ "lightningcss": "1.32.0",
+ "magic-string": "^0.30.21",
+ "source-map-js": "^1.2.1",
+ "tailwindcss": "4.3.3"
+ }
+ },
+ "node_modules/@tailwindcss/oxide": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz",
+ "integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 20"
+ },
+ "optionalDependencies": {
+ "@tailwindcss/oxide-android-arm64": "4.3.3",
+ "@tailwindcss/oxide-darwin-arm64": "4.3.3",
+ "@tailwindcss/oxide-darwin-x64": "4.3.3",
+ "@tailwindcss/oxide-freebsd-x64": "4.3.3",
+ "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3",
+ "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3",
+ "@tailwindcss/oxide-linux-arm64-musl": "4.3.3",
+ "@tailwindcss/oxide-linux-x64-gnu": "4.3.3",
+ "@tailwindcss/oxide-linux-x64-musl": "4.3.3",
+ "@tailwindcss/oxide-wasm32-wasi": "4.3.3",
+ "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3",
+ "@tailwindcss/oxide-win32-x64-msvc": "4.3.3"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-android-arm64": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.3.tgz",
+ "integrity": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-darwin-arm64": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.3.tgz",
+ "integrity": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-darwin-x64": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.3.tgz",
+ "integrity": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-freebsd-x64": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.3.tgz",
+ "integrity": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.3.tgz",
+ "integrity": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.3.tgz",
+ "integrity": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm64-musl": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.3.tgz",
+ "integrity": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-x64-gnu": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.3.tgz",
+ "integrity": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-x64-musl": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.3.tgz",
+ "integrity": "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.3.tgz",
+ "integrity": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==",
+ "bundleDependencies": [
+ "@napi-rs/wasm-runtime",
+ "@emnapi/core",
+ "@emnapi/runtime",
+ "@tybys/wasm-util",
+ "@emnapi/wasi-threads",
+ "tslib"
+ ],
+ "cpu": [
+ "wasm32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "^1.11.1",
+ "@emnapi/runtime": "^1.11.1",
+ "@emnapi/wasi-threads": "^1.2.2",
+ "@napi-rs/wasm-runtime": "^1.1.4",
+ "@tybys/wasm-util": "^0.10.2",
+ "tslib": "^2.8.1"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz",
+ "integrity": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-win32-x64-msvc": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.3.tgz",
+ "integrity": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/vite": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.3.tgz",
+ "integrity": "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@tailwindcss/node": "4.3.3",
+ "@tailwindcss/oxide": "4.3.3",
+ "tailwindcss": "4.3.3"
+ },
+ "peerDependencies": {
+ "vite": "^5.2.0 || ^6 || ^7 || ^8"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
+ "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "22.20.5",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.5.tgz",
+ "integrity": "sha512-U2+DNr+wSjpsTS/wZGYHq7GcwfuSmKiKvoPvK22zwTlRhU91yOniN4qRR5KhIjvif7ysw/dz/hKmfDH0Ris4aA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~6.21.0"
+ }
+ },
+ "node_modules/@vitejs/plugin-vue": {
+ "version": "5.2.4",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz",
+ "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "peerDependencies": {
+ "vite": "^5.0.0 || ^6.0.0",
+ "vue": "^3.2.25"
+ }
+ },
+ "node_modules/@volar/language-core": {
+ "version": "2.4.15",
+ "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.15.tgz",
+ "integrity": "sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@volar/source-map": "2.4.15"
+ }
+ },
+ "node_modules/@volar/source-map": {
+ "version": "2.4.15",
+ "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.15.tgz",
+ "integrity": "sha512-CPbMWlUN6hVZJYGcU/GSoHu4EnCHiLaXI9n8c9la6RaI9W5JHX+NqG+GSQcB0JdC2FIBLdZJwGsfKyBB71VlTg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@volar/typescript": {
+ "version": "2.4.15",
+ "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.15.tgz",
+ "integrity": "sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@volar/language-core": "2.4.15",
+ "path-browserify": "^1.0.1",
+ "vscode-uri": "^3.0.8"
+ }
+ },
+ "node_modules/@vue/compiler-core": {
+ "version": "3.5.44",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.44.tgz",
+ "integrity": "sha512-k34nCytbK8PQ1gR2mGROYOw6Pnv/mumqIdrZ3x2tbyXZDWm7ViKGLTfdB+DRrWqEQsLgGJvn0WN9jBif5zCTLQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.9",
+ "@vue/shared": "3.5.44",
+ "entities": "^7.0.1",
+ "estree-walker": "^2.0.2",
+ "source-map-js": "^1.2.2"
+ }
+ },
+ "node_modules/@vue/compiler-dom": {
+ "version": "3.5.44",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.44.tgz",
+ "integrity": "sha512-W5AoBFWdw1lQAKiHowkfgFKFab82oSRT6sGsPfw/q1GRjsy/ZJFLHGhJyBRIFN9APLTUduDH9U2JAiPOIyKuYg==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-core": "3.5.44",
+ "@vue/shared": "3.5.44"
+ }
+ },
+ "node_modules/@vue/compiler-sfc": {
+ "version": "3.5.44",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.44.tgz",
+ "integrity": "sha512-dzHmQ4jXTpdr5FjIQUeykVakN/uzqFbUo9/C5ikBFg3a3PeM6uRxeczx1ZbXBML0/zJstEh3ygYipzR5Z7b2Pw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.9",
+ "@vue/compiler-core": "3.5.44",
+ "@vue/compiler-dom": "3.5.44",
+ "@vue/compiler-ssr": "3.5.44",
+ "@vue/shared": "3.5.44",
+ "estree-walker": "^2.0.2",
+ "magic-string": "^0.30.21",
+ "postcss": "^8.5.29",
+ "source-map-js": "^1.2.2"
+ }
+ },
+ "node_modules/@vue/compiler-ssr": {
+ "version": "3.5.44",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.44.tgz",
+ "integrity": "sha512-xr5gb1JVHlfXJwSlRz1FeP6uxoazdzHuMTVLE7up+4Vrqxk219CvxLPxD9NXBt47yvZW7+p4DXfyp4O1fkByIA==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-dom": "3.5.44",
+ "@vue/shared": "3.5.44"
+ }
+ },
+ "node_modules/@vue/compiler-vue2": {
+ "version": "2.7.16",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz",
+ "integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "de-indent": "^1.0.2",
+ "he": "^1.2.0"
+ }
+ },
+ "node_modules/@vue/language-core": {
+ "version": "2.2.12",
+ "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.2.12.tgz",
+ "integrity": "sha512-IsGljWbKGU1MZpBPN+BvPAdr55YPkj2nB/TBNGNC32Vy2qLG25DYu/NBN2vNtZqdRbTRjaoYrahLrToim2NanA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@volar/language-core": "2.4.15",
+ "@vue/compiler-dom": "^3.5.0",
+ "@vue/compiler-vue2": "^2.7.16",
+ "@vue/shared": "^3.5.0",
+ "alien-signals": "^1.0.3",
+ "minimatch": "^9.0.3",
+ "muggle-string": "^0.4.1",
+ "path-browserify": "^1.0.1"
+ },
+ "peerDependencies": {
+ "typescript": "*"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vue/reactivity": {
+ "version": "3.5.44",
+ "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.44.tgz",
+ "integrity": "sha512-u+QJdKeBBEADkPdevsL+6E12QlYVfBD8Zcw5qjDQKtamI6l14KyqazJMobhCKelhWN5zpyKUiEDoTyHE93cqWw==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/shared": "3.5.44"
+ }
+ },
+ "node_modules/@vue/runtime-core": {
+ "version": "3.5.44",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.44.tgz",
+ "integrity": "sha512-uEa/ndwjhrrhnOfb2p+sZy6zeoamJOISqaLSJGk/8R6nABT1ZldpzcJoPzKj+zHVomVBPcmQYyC2r4LtvDV6CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/reactivity": "3.5.44",
+ "@vue/shared": "3.5.44"
+ }
+ },
+ "node_modules/@vue/runtime-dom": {
+ "version": "3.5.44",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.44.tgz",
+ "integrity": "sha512-3fc4ARcXVhVBaJ3ujWiNZJARD6pkyRcwG+ZYhMzjKtKUp7MAY0HLwr7r3Smep3Li6ZaWT6ewdmx8XqdaD5oNZg==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/reactivity": "3.5.44",
+ "@vue/runtime-core": "3.5.44",
+ "@vue/shared": "3.5.44",
+ "csstype": "^3.2.3"
+ }
+ },
+ "node_modules/@vue/server-renderer": {
+ "version": "3.5.44",
+ "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.44.tgz",
+ "integrity": "sha512-R4S1UoH4h5Frrauv8Z5XE5TxV9oBiUSgWIC4TyVK1FK+RocRnOLwFA5Xcmo2011RlgJp+AbZCnw6afrhlgRuUQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-ssr": "3.5.44",
+ "@vue/runtime-dom": "3.5.44",
+ "@vue/shared": "3.5.44"
+ }
+ },
+ "node_modules/@vue/shared": {
+ "version": "3.5.44",
+ "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.44.tgz",
+ "integrity": "sha512-H82XLH0DjmSOdbEwO15x3QTo1f5PWEtv237eLpYKc0mqLu+B8hnq16lFWmuBmNurkeiG3wr+oM1FhOll/YVFRg==",
+ "license": "MIT"
+ },
+ "node_modules/alien-signals": {
+ "version": "1.0.13",
+ "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-1.0.13.tgz",
+ "integrity": "sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/brace-expansion": {
+ "version": "2.1.7",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz",
+ "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "license": "MIT"
+ },
+ "node_modules/de-indent": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
+ "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/enhanced-resolve": {
+ "version": "5.26.0",
+ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.26.0.tgz",
+ "integrity": "sha512-9vhedylFonb2YGogzUKX6+Ja72gOJbN1QHAqdrvqLwhdl/QWbKopzoUC9EbQNVsAns/bx/4uyqalrQAoy1IByw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.4",
+ "tapable": "^2.3.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/entities": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
+ "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
+ "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.25.12",
+ "@esbuild/android-arm": "0.25.12",
+ "@esbuild/android-arm64": "0.25.12",
+ "@esbuild/android-x64": "0.25.12",
+ "@esbuild/darwin-arm64": "0.25.12",
+ "@esbuild/darwin-x64": "0.25.12",
+ "@esbuild/freebsd-arm64": "0.25.12",
+ "@esbuild/freebsd-x64": "0.25.12",
+ "@esbuild/linux-arm": "0.25.12",
+ "@esbuild/linux-arm64": "0.25.12",
+ "@esbuild/linux-ia32": "0.25.12",
+ "@esbuild/linux-loong64": "0.25.12",
+ "@esbuild/linux-mips64el": "0.25.12",
+ "@esbuild/linux-ppc64": "0.25.12",
+ "@esbuild/linux-riscv64": "0.25.12",
+ "@esbuild/linux-s390x": "0.25.12",
+ "@esbuild/linux-x64": "0.25.12",
+ "@esbuild/netbsd-arm64": "0.25.12",
+ "@esbuild/netbsd-x64": "0.25.12",
+ "@esbuild/openbsd-arm64": "0.25.12",
+ "@esbuild/openbsd-x64": "0.25.12",
+ "@esbuild/openharmony-arm64": "0.25.12",
+ "@esbuild/sunos-x64": "0.25.12",
+ "@esbuild/win32-arm64": "0.25.12",
+ "@esbuild/win32-ia32": "0.25.12",
+ "@esbuild/win32-x64": "0.25.12"
+ }
+ },
+ "node_modules/estree-walker": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
+ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
+ "license": "MIT"
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/he": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
+ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "he": "bin/he"
+ }
+ },
+ "node_modules/jiti": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz",
+ "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jiti": "lib/jiti-cli.mjs"
+ }
+ },
+ "node_modules/lightningcss": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
+ "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
+ "dev": true,
+ "license": "MPL-2.0",
+ "dependencies": {
+ "detect-libc": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "lightningcss-android-arm64": "1.32.0",
+ "lightningcss-darwin-arm64": "1.32.0",
+ "lightningcss-darwin-x64": "1.32.0",
+ "lightningcss-freebsd-x64": "1.32.0",
+ "lightningcss-linux-arm-gnueabihf": "1.32.0",
+ "lightningcss-linux-arm64-gnu": "1.32.0",
+ "lightningcss-linux-arm64-musl": "1.32.0",
+ "lightningcss-linux-x64-gnu": "1.32.0",
+ "lightningcss-linux-x64-musl": "1.32.0",
+ "lightningcss-win32-arm64-msvc": "1.32.0",
+ "lightningcss-win32-x64-msvc": "1.32.0"
+ }
+ },
+ "node_modules/lightningcss-android-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
+ "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
+ "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
+ "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-freebsd-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
+ "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm-gnueabihf": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
+ "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
+ "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
+ "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
+ "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
+ "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-arm64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
+ "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-x64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
+ "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lucide-vue-next": {
+ "version": "0.468.0",
+ "resolved": "https://registry.npmjs.org/lucide-vue-next/-/lucide-vue-next-0.468.0.tgz",
+ "integrity": "sha512-quV/6T8YB1XK0VOEnebg3Byd8Rsan5/m95cvjnuHV4vcS3qEnLAybkrSh0hk3ppavx+V7R1PjNW+mGDvcBdz4A==",
+ "deprecated": "Package deprecated. Please use @lucide/vue instead.",
+ "license": "ISC",
+ "peerDependencies": {
+ "vue": ">=3.0.1"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "9.0.9",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
+ "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/muggle-string": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz",
+ "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.20",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz",
+ "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/path-browserify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
+ "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz",
+ "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.30",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.30.tgz",
+ "integrity": "sha512-zkd+e9S1AQHhmaBKye9zlFHWjOwVgqXl1q3ECYy8561K7qxR2cREf/STLskulBFd78k6AOFeA+ff1TtEzk9bAA==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.20",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.2"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.64.6",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.64.6.tgz",
+ "integrity": "sha512-fX1Vy+UNKdXn+xKDnu/HBXbHVr1AY8cSdQj9BU4kLv5aFCMqGpvrVbyUMmjN3DdzWhSAuTbcX8PCNEVaumthYA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.9"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@napi-rs/lzma-linux-x64-gnu": "1.5.1",
+ "@rollup/rollup-android-arm-eabi": "4.64.6",
+ "@rollup/rollup-android-arm64": "4.64.6",
+ "@rollup/rollup-darwin-arm64": "4.64.6",
+ "@rollup/rollup-darwin-x64": "4.64.6",
+ "@rollup/rollup-freebsd-arm64": "4.64.6",
+ "@rollup/rollup-freebsd-x64": "4.64.6",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.64.6",
+ "@rollup/rollup-linux-arm-musleabihf": "4.64.6",
+ "@rollup/rollup-linux-arm64-gnu": "4.64.6",
+ "@rollup/rollup-linux-arm64-musl": "4.64.6",
+ "@rollup/rollup-linux-loong64-gnu": "4.64.6",
+ "@rollup/rollup-linux-loong64-musl": "4.64.6",
+ "@rollup/rollup-linux-ppc64-gnu": "4.64.6",
+ "@rollup/rollup-linux-ppc64-musl": "4.64.6",
+ "@rollup/rollup-linux-riscv64-gnu": "4.64.6",
+ "@rollup/rollup-linux-riscv64-musl": "4.64.6",
+ "@rollup/rollup-linux-s390x-gnu": "4.64.6",
+ "@rollup/rollup-linux-x64-gnu": "4.64.6",
+ "@rollup/rollup-linux-x64-musl": "4.64.6",
+ "@rollup/rollup-openbsd-x64": "4.64.6",
+ "@rollup/rollup-openharmony-arm64": "4.64.6",
+ "@rollup/rollup-win32-arm64-msvc": "4.64.6",
+ "@rollup/rollup-win32-ia32-msvc": "4.64.6",
+ "@rollup/rollup-win32-x64-gnu": "4.64.6",
+ "@rollup/rollup-win32-x64-msvc": "4.64.6",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz",
+ "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/tailwindcss": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz",
+ "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tapable": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz",
+ "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.17",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
+ "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "devOptional": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/vite": {
+ "version": "6.4.4",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.4.tgz",
+ "integrity": "sha512-xGjU/SyjeNyb0E4KxXBvYz/1C+3JXuujvf2hXNOtnlcE+58o5ajd9N2/n2VhkiJvpCP2qQasnUpx5PXNeI+p1Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.25.0",
+ "fdir": "^6.4.4",
+ "picomatch": "^4.0.2",
+ "postcss": "^8.5.3",
+ "rollup": "^4.34.9",
+ "tinyglobby": "^0.2.13"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
+ "jiti": ">=1.21.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vscode-uri": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.2.0.tgz",
+ "integrity": "sha512-m2gXo3bn0G1kT9InzMf07fTbqMbGtyckj3bH5ktLO+1Ssv+yiATZ4dhwaQv9UZWxJh6E9IFGnQyjgWVDWVBDrg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/vue": {
+ "version": "3.5.44",
+ "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.44.tgz",
+ "integrity": "sha512-dvvmaOCG7lAMkJTPqPwWeGMsPS6DIsFsfZDUgBp9V6mHf9b3+/wyVwYR4Es+WV6uJC0FKRxp81Obcq0ZwUcGFQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-dom": "3.5.44",
+ "@vue/compiler-sfc": "3.5.44",
+ "@vue/runtime-dom": "3.5.44",
+ "@vue/server-renderer": "3.5.44",
+ "@vue/shared": "3.5.44"
+ },
+ "peerDependencies": {
+ "typescript": "*"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vue-tsc": {
+ "version": "2.2.12",
+ "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.2.12.tgz",
+ "integrity": "sha512-P7OP77b2h/Pmk+lZdJ0YWs+5tJ6J2+uOQPo7tlBnY44QqQSPYvS0qVT4wqDJgwrZaLe47etJLLQRFia71GYITw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@volar/typescript": "2.4.15",
+ "@vue/language-core": "2.2.12"
+ },
+ "bin": {
+ "vue-tsc": "bin/vue-tsc.js"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.0.0"
+ }
+ }
+ }
+}
diff --git a/projects/mila/package.json b/projects/mila/package.json
new file mode 100644
index 0000000..602a013
--- /dev/null
+++ b/projects/mila/package.json
@@ -0,0 +1,25 @@
+{
+ "name": "mila-tracking",
+ "private": true,
+ "version": "1.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vue-tsc -b && vite build",
+ "preview": "vite preview",
+ "typecheck": "vue-tsc --noEmit"
+ },
+ "dependencies": {
+ "lucide-vue-next": "^0.468.0",
+ "vue": "^3.5.13"
+ },
+ "devDependencies": {
+ "@tailwindcss/vite": "^4.0.9",
+ "@types/node": "^22.10.2",
+ "@vitejs/plugin-vue": "^5.2.1",
+ "tailwindcss": "^4.0.9",
+ "typescript": "^5.7.2",
+ "vite": "^6.2.0",
+ "vue-tsc": "^2.2.0"
+ }
+}
diff --git a/projects/mila/public/favicon.ico b/projects/mila/public/favicon.ico
new file mode 100644
index 0000000..b5ca0ad
Binary files /dev/null and b/projects/mila/public/favicon.ico differ
diff --git a/projects/mila/public/manifest.json b/projects/mila/public/manifest.json
new file mode 100644
index 0000000..950832d
--- /dev/null
+++ b/projects/mila/public/manifest.json
@@ -0,0 +1,48 @@
+{
+ "id": "/tracking/mila/",
+ "name": "Mila — Schlaf & Entwicklung",
+ "short_name": "Mila",
+ "description": "Schlaf-, Trink- und Windeltagebuch für Mila Graf",
+ "start_url": "/tracking/mila/",
+ "scope": "/tracking/mila/",
+ "display": "standalone",
+ "orientation": "portrait-primary",
+ "background_color": "#090A0F",
+ "theme_color": "#090A0F",
+ "icons": [
+ {
+ "src": "/tracking/mila/mila-icon-192-v3.png",
+ "sizes": "192x192",
+ "type": "image/png",
+ "purpose": "any"
+ },
+ {
+ "src": "/tracking/mila/mila-icon-192-v3.png",
+ "sizes": "192x192",
+ "type": "image/png",
+ "purpose": "maskable"
+ },
+ {
+ "src": "/tracking/mila/mila-icon-512-v3.png",
+ "sizes": "512x512",
+ "type": "image/png",
+ "purpose": "any"
+ },
+ {
+ "src": "/tracking/mila/mila-icon-512-v3.png",
+ "sizes": "512x512",
+ "type": "image/png",
+ "purpose": "maskable"
+ },
+ {
+ "src": "/tracking/mila/mila-apple-touch-icon-v3.png",
+ "sizes": "180x180",
+ "type": "image/png"
+ },
+ {
+ "src": "/tracking/mila/mila-icon.svg",
+ "sizes": "any",
+ "type": "image/svg+xml"
+ }
+ ]
+}
diff --git a/projects/mila/public/mila-apple-touch-icon-v3.png b/projects/mila/public/mila-apple-touch-icon-v3.png
new file mode 100644
index 0000000..9671ff8
Binary files /dev/null and b/projects/mila/public/mila-apple-touch-icon-v3.png differ
diff --git a/projects/mila/public/mila-apple-touch-icon.png b/projects/mila/public/mila-apple-touch-icon.png
new file mode 100644
index 0000000..9671ff8
Binary files /dev/null and b/projects/mila/public/mila-apple-touch-icon.png differ
diff --git a/projects/mila/public/mila-icon-16-v3.png b/projects/mila/public/mila-icon-16-v3.png
new file mode 100644
index 0000000..4bf8af7
Binary files /dev/null and b/projects/mila/public/mila-icon-16-v3.png differ
diff --git a/projects/mila/public/mila-icon-16.png b/projects/mila/public/mila-icon-16.png
new file mode 100644
index 0000000..4bf8af7
Binary files /dev/null and b/projects/mila/public/mila-icon-16.png differ
diff --git a/projects/mila/public/mila-icon-192-v3.png b/projects/mila/public/mila-icon-192-v3.png
new file mode 100644
index 0000000..9b1971b
Binary files /dev/null and b/projects/mila/public/mila-icon-192-v3.png differ
diff --git a/projects/mila/public/mila-icon-192.png b/projects/mila/public/mila-icon-192.png
new file mode 100644
index 0000000..9b1971b
Binary files /dev/null and b/projects/mila/public/mila-icon-192.png differ
diff --git a/projects/mila/public/mila-icon-32-v3.png b/projects/mila/public/mila-icon-32-v3.png
new file mode 100644
index 0000000..9d47cf3
Binary files /dev/null and b/projects/mila/public/mila-icon-32-v3.png differ
diff --git a/projects/mila/public/mila-icon-32.png b/projects/mila/public/mila-icon-32.png
new file mode 100644
index 0000000..9d47cf3
Binary files /dev/null and b/projects/mila/public/mila-icon-32.png differ
diff --git a/projects/mila/public/mila-icon-512-v3.png b/projects/mila/public/mila-icon-512-v3.png
new file mode 100644
index 0000000..23a18c5
Binary files /dev/null and b/projects/mila/public/mila-icon-512-v3.png differ
diff --git a/projects/mila/public/mila-icon-512.png b/projects/mila/public/mila-icon-512.png
new file mode 100644
index 0000000..23a18c5
Binary files /dev/null and b/projects/mila/public/mila-icon-512.png differ
diff --git a/projects/mila/public/mila-icon.svg b/projects/mila/public/mila-icon.svg
new file mode 100644
index 0000000..a7fc212
--- /dev/null
+++ b/projects/mila/public/mila-icon.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/projects/mila/public/mila.html b/projects/mila/public/mila.html
new file mode 100644
index 0000000..f318937
--- /dev/null
+++ b/projects/mila/public/mila.html
@@ -0,0 +1,1185 @@
+
+
+
+
+
+
+
+ Mila — Schlaf & Entwicklung
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Mila App installieren
+
Direkt auf dem Homescreen nutzen
+
+
+
+
+ Installieren
+
+
+
+
+
+
+
+
+
+
+
+
+
📱
+
+ Als App nutzen: Im Safari-Menü auf Teilen und „Zum Home-Bildschirm“ (+) tippen.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 6 Monate, 4 Wochen alt
+
+
+
Dienstag, 13.10.2026
+
+
+
+
+
+
Wach (11 h 11 min )
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ☀️ Status: Wach & Munter
+
+
+
+ Letztes Schläfchen beendet um 18:15 Uhr.
+
+
+
+
+
+
+
+
24h Schlaf Heute
+
+
Nacht + Schläfchen
+
+
+
Tagesschläfchen
+
+
0 min gesamt
+
+
+
Nachtschlaf
+
+
Hauptschlafphase
+
+
+
7-Tage-Schnitt
+
+
Alterskorridor: 12–15h
+
+
+
+
+
+
+
+ 📋
+
Heutiger Tagesablauf im Detail
+
+
2026-10-13
+
+
+
+
+
+ 🌿
+ Tagesschläfchen (0 erfasst)
+
+
+
+
+ Heute bisher noch kein Tagesschläfchen eingetragen.
+
+
+
+
+
+
+
+
+ 🌙
+ Nachtschlaf
+
+
+
+
+ Kein Nachtschlaf für das Tagesdatum zugeordnet (Nachtstart zählt zum Vortag).
+
+
+
+
+
+
+
+
+
+
+
+ 🍼
+
Fläschchen heute
+
+
210 ml (1x)
+
+
+ 04:15 Uhr (210 ml)
+
+
+
+
+
+
+
+
+
+
+ 📈
+
Entwicklungs-Benchmarks & Tendenzen
+
+
+
+
7-Tage-Ø Gesamtschlaf
+
12.12 h
+
Nacht: 9.93 h · Tag: 3.1 Naps (2.19 h)
+
+
+
30-Tage-Ø Gesamtschlaf
+
13.77 h
+
Stabile zirkadiane Konsolidierung
+
+
+
Altersreferenz (7 Monate)
+
12 – 15 Std
+
Typisch 2–3 Naps, Wachfenster ~2.25–3.25h
+
+
+
+
+
+
+
+
+ 📅
+
Historie (Letzte 30 Tage)
+
+
DuckDB Aggregatdaten
+
+
+
+
+
+
+ Datum
+ 24h Gesamt
+ Nacht
+ Schläfchen
+ Fläschchen
+ Windeln
+
+
+
+
+
+
+ Mo, 12.10.
+
+
+ 2.67 h
+ 0 min
+ 4 (160 min)
+ 540 ml (4x)
+ 1 (0 Stuhl)
+
+
+
+ Einzelschläfchen: 08:05–08:39 (34m) 11:32–12:02 (30m) 14:11–15:22 (71m) 17:50–18:15 (25m)
+
+
+
+
+
+ So, 11.10.
+
+
+ 12.22 h
+ 10 h 45 min
+ 3 (88 min)
+ 170 ml (1x)
+ 1 (1 Stuhl)
+
+
+
+ Einzelschläfchen: 09:10–09:35 (25m) 11:05–11:40 (35m) 13:12–13:40 (28m) 19:25–06:10 (645m)
+
+
+
+
+
+ Sa, 10.10.
+
+
+ 14.35 h
+ 11 h 40 min
+ 2 (161 min)
+ —
+ —
+
+
+
+ Einzelschläfchen: 12:22–14:25 (123m) 16:10–16:48 (38m) 19:20–07:00 (700m)
+
+
+
+
+
+ Fr, 09.10.
+
+
+ 12.42 h
+ 11 h 20 min
+ 3 (65 min)
+ —
+ —
+
+
+
+ Einzelschläfchen: 12:35–12:55 (20m) 15:20–15:35 (15m) 17:10–17:40 (30m) 20:00–07:20 (680m)
+
+
+
+
+
+ Do, 08.10.
+
+
+ 14.67 h
+ 11 h 45 min
+ 3 (175 min)
+ —
+ —
+
+
+
+ Einzelschläfchen: 09:49–10:24 (35m) 11:55–12:22 (27m) 15:05–16:58 (113m) 19:15–07:00 (705m)
+
+
+
+
+
+ Mi, 07.10.
+
+
+ 14.13 h
+ 12 h 20 min
+ 3 (108 min)
+ —
+ —
+
+
+
+ Einzelschläfchen: 08:50–09:20 (30m) 11:21–12:01 (40m) 14:20–14:58 (38m) 18:40–07:00 (740m)
+
+
+
+
+
+ Di, 06.10.
+
+
+ 14.38 h
+ 11 h 40 min
+ 4 (163 min)
+ —
+ —
+
+
+
+ Einzelschläfchen: 08:58–09:38 (40m) 10:45–10:55 (10m) 12:08–12:41 (33m) 14:29–15:49 (80m) 19:20–07:00 (700m)
+
+
+
+
+
+ Mo, 05.10.
+
+
+ 4.18 h
+ 55 min
+ 4 (196 min)
+ 0 ml (2x)
+ —
+
+
+
+ Einzelschläfchen: 06:20–07:15 (55m) 08:12–09:00 (48m) 10:54–12:27 (93m) 14:36–15:06 (30m) 16:29–16:54 (25m)
+
+
+
+
+
+ So, 04.10.
+
+
+ 1.88 h
+ 0 min
+ 4 (113 min)
+ —
+ —
+
+
+
+ Einzelschläfchen: 08:10–08:50 (40m) 10:50–10:51 (1m) 13:40–14:05 (25m) 15:20–16:07 (47m)
+
+
+
+
+
+ Sa, 03.10.
+
+
+ 12.78 h
+ 10 h 05 min
+ 5 (162 min)
+ —
+ —
+
+
+
+ Einzelschläfchen: 09:14–09:39 (25m) 10:59–12:00 (61m) 13:30–13:58 (28m) 14:52–15:20 (28m) 17:10–17:30 (20m) 20:10–06:15 (605m)
+
+
+
+
+
+ Fr, 02.10.
+
+
+ 13.02 h
+ 9 h 30 min
+ 4 (211 min)
+ —
+ —
+
+
+
+ Einzelschläfchen: 09:00–09:30 (30m) 11:25–11:49 (24m) 13:13–13:30 (17m) 14:55–17:15 (140m) 20:30–06:00 (570m)
+
+
+
+
+
+ Do, 01.10.
+
+
+ 11.20 h
+ 9 h 30 min
+ 3 (102 min)
+ —
+ —
+
+
+
+ Einzelschläfchen: 09:30–10:06 (36m) 11:30–12:00 (30m) 14:00–14:36 (36m) 21:00–06:30 (570m)
+
+
+
+
+
+ Mi, 30.09.
+
+
+ 13.13 h
+ 11 h 17 min
+ 3 (111 min)
+ —
+ —
+
+
+
+ Einzelschläfchen: 09:45–10:47 (62m) 13:18–13:38 (20m) 16:07–16:36 (29m) 19:13–06:30 (677m)
+
+
+
+
+
+ Di, 29.09.
+
+
+ 13.83 h
+ 11 h 53 min
+ 4 (117 min)
+ 0 ml (1x)
+ —
+
+
+
+ Einzelschläfchen: 09:51–10:38 (47m) 11:36–11:51 (15m) 13:35–14:05 (30m) 16:00–16:25 (25m) 18:57–06:50 (713m)
+
+
+
+
+
+ Mo, 28.09.
+
+
+ 26.10 h
+ 23 h 34 min
+ 4 (152 min)
+ —
+ —
+
+
+
+ Einzelschläfchen: 08:27–09:17 (50m) 10:52–11:40 (48m) 13:36–14:06 (30m) 16:05–16:29 (24m) 19:13–07:00 (707m) 19:13–07:00 (707m)
+
+
+
+
+
+ So, 27.09.
+
+
+ 12.88 h
+ 10 h 10 min
+ 4 (163 min)
+ —
+ —
+
+
+
+ Einzelschläfchen: 09:10–09:30 (20m) 10:50–11:25 (35m) 13:39–15:07 (88m) 17:50–18:10 (20m) 19:55–06:05 (610m)
+
+
+
+
+
+ Sa, 26.09.
+
+
+ 26.67 h
+ 23 h 50 min
+ 3 (170 min)
+ 0 ml (1x)
+ —
+
+
+
+ Einzelschläfchen: 08:54–09:40 (46m) 12:38–14:07 (89m) 15:50–16:25 (35m) 18:55–06:50 (715m) 18:55–06:50 (715m)
+
+
+
+
+
+ Fr, 25.09.
+
+
+ 15.13 h
+ 11 h 33 min
+ 5 (215 min)
+ —
+ —
+
+
+
+ Einzelschläfchen: 07:39–08:08 (29m) 10:07–10:31 (24m) 10:40–12:39 (119m) 14:40–15:01 (21m) 16:43–17:05 (22m) 19:07–06:40 (693m)
+
+
+
+
+
+ Do, 24.09.
+
+
+ 24.52 h
+ 23 h 02 min
+ 4 (89 min)
+ —
+ —
+
+
+
+ Einzelschläfchen: 09:02–09:34 (32m) 12:31–12:55 (24m) 13:00–13:01 (1m) 14:47–15:19 (32m) 18:34–06:05 (691m) 18:34–06:05 (691m)
+
+
+
+
+
+ Mi, 23.09.
+
+
+ 13.68 h
+ 11 h 18 min
+ 4 (143 min)
+ —
+ —
+
+
+
+ Einzelschläfchen: 08:30–08:59 (29m) 11:40–11:55 (15m) 13:40–14:50 (70m) 16:36–17:05 (29m) 19:27–06:45 (678m)
+
+
+
+
+
+ Di, 22.09.
+
+
+ 25.07 h
+ 23 h 12 min
+ 5 (112 min)
+ 0 ml (1x)
+ —
+
+
+
+ Einzelschläfchen: 08:53–09:23 (30m) 09:53–09:59 (6m) 12:15–12:40 (25m) 14:35–15:05 (30m) 17:06–17:27 (21m) 18:51–06:27 (696m) 18:51–06:27 (696m)
+
+
+
+
+
+ Mo, 21.09.
+
+
+ 3.48 h
+ 0 min
+ 5 (209 min)
+ —
+ —
+
+
+
+ Einzelschläfchen: 08:00–08:10 (10m) 09:47–10:19 (32m) 10:34–12:25 (111m) 14:25–14:49 (24m) 15:43–16:15 (32m)
+
+
+
+
+
+ So, 20.09.
+
+
+ 14.02 h
+ 10 h 55 min
+ 5 (186 min)
+ —
+ —
+
+
+
+ Einzelschläfchen: 06:40–06:41 (1m) 08:50–09:30 (40m) 11:00–11:35 (35m) 14:00–15:25 (85m) 16:40–17:05 (25m) 19:30–06:25 (655m)
+
+
+
+
+
+ Sa, 19.09.
+
+
+ 14.02 h
+ 11 h 55 min
+ 4 (126 min)
+ —
+ —
+
+
+
+ Einzelschläfchen: 06:30–06:31 (1m) 09:10–09:40 (30m) 10:55–12:00 (65m) 14:45–15:15 (30m) 18:45–06:40 (715m)
+
+
+
+
+
+
+
+
+
+
+
+
+ { window.initGrowthCharts && window.initGrowthCharts(); }); }"
+ class="w-full p-5 flex items-center justify-between text-left hover:bg-zinc-800/30 transition focus:outline-none"
+ >
+
+
+ 📏
+
+
+
+
Körpergröße & Gewicht (WHO-Perzentilen)
+
+ 0–24 Monate
+
+
+
+ Vergleich mit den offiziellen WHO-Standards für Mädchen (P3, P50 Median, P97)
+
+
+
+
+
+
+ Noch keine Messungen erfasst
+
+
+
+
+
+
+
+
+
+
+
+
+ Noch keine Messungen erfasst
+
+
+
+
+
+
+
+ { window.updateGrowthChartTab && window.updateGrowthChartTab('weight'); });"
+ :class="growthTab === 'weight' ? 'bg-gradient-to-r from-pink-500 to-purple-600 text-white shadow-md font-semibold' : 'text-zinc-400 hover:text-white'"
+ class="px-4 py-1.5 rounded-lg text-xs transition"
+ >
+ ⚖️ Gewicht (kg)
+
+ { window.updateGrowthChartTab && window.updateGrowthChartTab('height'); });"
+ :class="growthTab === 'height' ? 'bg-gradient-to-r from-pink-500 to-purple-600 text-white shadow-md font-semibold' : 'text-zinc-400 hover:text-white'"
+ class="px-4 py-1.5 rounded-lg text-xs transition"
+ >
+ 📏 Körpergröße (cm)
+
+
+
+
+
+
+ Mila
+
+
+ WHO Median (P50)
+
+
+ P3–P97 Normkorridor (94%)
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 📋
+ Erfasste Messungen
+
+ 0 Messung(en)
+
+
+
+
+
+
+ Datum
+ Alter
+ Gewicht
+ Körpergröße
+ Notiz / Anlass
+
+
+
+
+
+
+ Bisher noch keine manuellen Messwerte eingetragen. Messungen können per Telegram oder CLI erfasst werden.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Aktualisiert: 13.10.2026 um 05:26:47 Uhr · Live Sync
+ Tipp: Im Safari-Menü auf „Zum Home-Bildschirm" tippen für die native App-Nutzung.
+
+
+
+
+
diff --git a/projects/mila/public/mila.webmanifest b/projects/mila/public/mila.webmanifest
new file mode 100644
index 0000000..950832d
--- /dev/null
+++ b/projects/mila/public/mila.webmanifest
@@ -0,0 +1,48 @@
+{
+ "id": "/tracking/mila/",
+ "name": "Mila — Schlaf & Entwicklung",
+ "short_name": "Mila",
+ "description": "Schlaf-, Trink- und Windeltagebuch für Mila Graf",
+ "start_url": "/tracking/mila/",
+ "scope": "/tracking/mila/",
+ "display": "standalone",
+ "orientation": "portrait-primary",
+ "background_color": "#090A0F",
+ "theme_color": "#090A0F",
+ "icons": [
+ {
+ "src": "/tracking/mila/mila-icon-192-v3.png",
+ "sizes": "192x192",
+ "type": "image/png",
+ "purpose": "any"
+ },
+ {
+ "src": "/tracking/mila/mila-icon-192-v3.png",
+ "sizes": "192x192",
+ "type": "image/png",
+ "purpose": "maskable"
+ },
+ {
+ "src": "/tracking/mila/mila-icon-512-v3.png",
+ "sizes": "512x512",
+ "type": "image/png",
+ "purpose": "any"
+ },
+ {
+ "src": "/tracking/mila/mila-icon-512-v3.png",
+ "sizes": "512x512",
+ "type": "image/png",
+ "purpose": "maskable"
+ },
+ {
+ "src": "/tracking/mila/mila-apple-touch-icon-v3.png",
+ "sizes": "180x180",
+ "type": "image/png"
+ },
+ {
+ "src": "/tracking/mila/mila-icon.svg",
+ "sizes": "any",
+ "type": "image/svg+xml"
+ }
+ ]
+}
diff --git a/projects/mila/public/sw.js b/projects/mila/public/sw.js
new file mode 100644
index 0000000..5c7ea30
--- /dev/null
+++ b/projects/mila/public/sw.js
@@ -0,0 +1,51 @@
+// Minimal service worker for Mila PWA with instant cache-bypassing navigation
+const CACHE_NAME = 'mila-pwa-v5';
+
+self.addEventListener('install', (event) => {
+ self.skipWaiting();
+});
+
+self.addEventListener('activate', (event) => {
+ event.waitUntil(
+ caches.keys().then((keys) =>
+ Promise.all(
+ keys.map((key) => {
+ if (key !== CACHE_NAME) return caches.delete(key);
+ })
+ )
+ ).then(() => self.clients.claim())
+ );
+});
+
+self.addEventListener('fetch', (event) => {
+ if (event.request.method !== 'GET') return;
+
+ // For HTML documents/navigations: ALWAYS bypass HTTP cache to get latest live server rendering
+ if (event.request.destination === 'document' || event.request.mode === 'navigate') {
+ event.respondWith(
+ fetch(event.request, { cache: 'no-cache' })
+ .then((response) => {
+ if (response && response.status === 200) {
+ const clone = response.clone();
+ caches.open(CACHE_NAME).then((cache) => cache.put(event.request, clone));
+ }
+ return response;
+ })
+ .catch(() => caches.match(event.request))
+ );
+ return;
+ }
+
+ // Assets (images, fonts, scripts): network-first with offline cache fallback
+ event.respondWith(
+ fetch(event.request)
+ .then((response) => {
+ if (response && response.status === 200) {
+ const clone = response.clone();
+ caches.open(CACHE_NAME).then((cache) => cache.put(event.request, clone));
+ }
+ return response;
+ })
+ .catch(() => caches.match(event.request))
+ );
+});
diff --git a/projects/mila/src/App.vue b/projects/mila/src/App.vue
new file mode 100644
index 0000000..1e2c45c
--- /dev/null
+++ b/projects/mila/src/App.vue
@@ -0,0 +1,1000 @@
+
+
+
+
+
+
+
+
+
+
+ Git-Synchronisationskonflikt: Die Datenänderungen konnten nicht automatisch zusammengeführt werden.
+
+
+
+
+
+
+
+ {{ fetchError }}
+ Erneut versuchen
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ data.metadata.age_str }}
+
+
+
{{ data?.metadata?.today_pretty || 'Lade...' }}
+
+
+
+
+
+
+ Installieren
+
+
+
+
+ {{ data.live_status.is_sleeping ? 'Schläft' : 'Wach' }}
+ ({{ liveCounterStr }})
+
+
+
+
+
+
+
+
+ Erfassen
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Eltern-Cockpit: Sofort-Überblick
+
+
+
+
+
+
+
+
+
+ {{ data.live_status.is_sleeping ? 'Schläft gerade' : 'Wachfenster' }}
+
+
+ {{ wakeWindowStatus.text }}
+
+
+
+
+
+
+
+ Gestartet um {{ data.live_status.active_sleep_start_time }} Uhr
+
+
+ Aufgewacht um {{ data.live_status.last_wake_time_str || '—' }} Uhr
+
+ {{ wakeWindowStatus.hint }}
+
+
+
+
+
+
+
+
+ Aufgewacht! (Schlaf beenden)
+
+
+
+
+
+
+
+
+
+ Letzte Mahlzeit
+
+
+ {{ feedIntervalStatus?.isDue ? 'Hunger-Fenster' : '3–4h Takt' }}
+
+
+
+
+
+
+
+ {{ data.live_status.last_feed_amount ? data.live_status.last_feed_amount + ' ' : '' }}{{ data.live_status.last_feed_item || 'Fläschchen' }} um {{ data.live_status.last_feed_time }} Uhr
+
+ Heute noch keine Nahrung
+
+
+
+
+ Heute gesamt:
+ {{ data.today.feeding_summary.total_consumed_ml }} ml · {{ data.today.feeding_summary.total_solid_g }} g
+
+
+
+
+
+
+
+
+ Windel & Stuhl
+
+
+ {{ data.today.diaper_summary.total_changes }} gewechselt
+
+
+
+
+
+
+ Stuhlgang: {{ diaperStatus?.lastStoolStr }}
+
+
+
+
+ Stuhl-Quote heute:
+ {{ data.today.diaper_summary.dirty_count }} von {{ data.today.diaper_summary.total_changes }}
+
+
+
+
+
+
+
+
+
24h Schlaf Heute
+
+
Nacht + Schläfchen
+
+
+
Tagesschläfchen
+
+
{{ data.today.nap_minutes }} min gesamt
+
+
+
Nachtschlaf
+
+
Hauptschlafphase
+
+
+
7-Tage-Schnitt
+
+
Norm: 12–15h
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 📋
+
Heutiger Tagesablauf
+ ({{ data.today.naps.length }} Naps · {{ data.today.feedings.length }} Mahlzeiten · {{ data.today.diapers.length }} Windeln)
+
+
+
+
+
+
+
+
+
+ 🌿
+ Tagesschläfchen ({{ data.today.naps.length }} erfasst)
+
+
+
+
+
+ #{{ idx + 1 }}
+
+
+
+ {{ n.start }} – {{ n.end || 'läuft gerade' }} Uhr
+
+
+
+ ({{ n.location }})
+ · {{ n.note }}
+
+
+
+
+
+ {{ n.duration_minutes ? `${n.duration_minutes} min` : 'in Ausführung' }}
+
+
+
+
+
+
+
+
+ Heute bisher noch kein Tagesschläfchen eingetragen.
+
+
+
+
+
+
+
+ 🍼🥣
+ Ernährung heute
+
+
+ {{ data.today.feeding_summary.total_consumed_ml }} ml
+ {{ data.today.feeding_summary.total_solid_g }} g Brei
+
+
+
+
+ {{ f.kind === 'feeding.bottle' ? '🍼' : '🥣' }}
+
+ {{ f.time }} Uhr: {{ f.consumed_ml ? `${f.consumed_ml} ml` : (f.solid_g ? `${f.item ? f.item + ' · ' : ''}${f.solid_g} g` : (f.item || 'Brei')) }}
+
+
+
+
+
+
+
+
+
+
+
+
+ 🧷
+ Windeln heute
+
+
+ {{ data.today.diaper_summary.total_changes }} gewechselt ({{ data.today.diaper_summary.wet_count }} nass · {{ data.today.diaper_summary.dirty_count }} Stuhl)
+
+
+
+
+ {{ d.time }} Uhr ({{ d.pee && d.poo ? 'Pipi+Kacka' : (d.poo ? 'Kacka' : 'Pipi') }})
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
📅
+
+
Schlaf- & Tagesverlauf
+
Tippen für Einzelschläfchen-Details & Vollanalyse
+
+
+
+
+
+
+
+ {{ p === '7d' ? '7 Tage' : p === '14d' ? '14 Tage' : '30 Tage' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ row.weekday }}, {{ row.formatted_date }}
+
+
+ Heute
+
+
+
+ 🌙 {{ row.night_duration_str }}
+ ·
+ 🌿 {{ row.nap_str }}
+
+
+
+
+
+
+
+
+
+
+
+
+ ⏱️
+ Erfasste Schlafphasen:
+
+
+ Tages-Detail öffnen →
+
+
+
+
+ {{ s.type === 'night' ? '🌙' : '🌿' }}
+ {{ s.start }}–{{ s.end }}
+ ({{ s.duration_minutes }}m)
+ · {{ s.location }}
+
+
+
+ Keine Einzelschläfchen für diesen Tag hinterlegt.
+
+
+
+
+
+
+ 🍼 {{ row.feed_bottles_ml }} ml ({{ row.feed_bottles_count }}x)
+
+
+ 🥣 {{ row.feed_solid_g }} g ({{ row.feed_solid_count }}x)
+
+
+
+ 🧷 {{ row.diaper_changes }} Windeln ({{ row.diaper_dirty }} Stuhl)
+
+
+
+
+
+
+
+
+
+
+
+ Datum
+ Schlaf 24h
+ Nacht
+ Naps
+ Nahrung
+ Windeln
+ Detail
+
+
+
+
+
+ {{ row.weekday }}, {{ row.formatted_date }}
+ Heute
+
+ {{ row.total_hours.toFixed(2) }} h
+ {{ row.night_duration_str }}
+ {{ row.nap_str }}
+
+ {{ row.feed_bottles_count > 0 ? `${row.feed_bottles_ml}ml` : (row.feed_solid_count > 0 ? `${row.feed_solid_g}g` : '—') }}
+
+
+ {{ row.diaper_changes > 0 ? `${row.diaper_changes} (${row.diaper_dirty})` : '—' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Lade Mila-Dashboard aus DuckDB...
+
+
+
+
+
+
+ Erfassen
+
+
+
+
+
+
+
+
+
+
+
+
+ Tagesprotokoll: {{ selectedDayDetail ? `${selectedDayDetail.weekday}, ${selectedDayDetail.date}` : 'Lade...' }}
+
+
Vollständige Ereignisse dieses Tages
+
+
+
+
+
+
+
+
+ Lade Tagesereignisse...
+
+
+
+
+
+
+
+ Schlafphasen ({{ selectedDayDetail.sleep_sessions.length }})
+
+
+
+
+
+ {{ s.sleep_type === 'night' ? '🌙 Nachtschlaf' : '🌿 Tagesschläfchen' }}: {{ s.start }} – {{ s.end }} Uhr
+
+
+ Ort: {{ s.location }}
+ · {{ s.note }}
+
+
+
+ {{ s.duration_minutes }} min
+
+
+
+
+
+
+
Keine Schlafphasen verzeichnet.
+
+
+
+
+
+
+ Mahlzeiten ({{ selectedDayDetail.feedings.length }})
+
+
+
+
+
+ {{ f.kind === 'feeding.bottle' ? '🍼 Fläschchen' : '🥣 Brei' }}: {{ f.time }} Uhr
+
+
+ {{ f.consumed_ml ? `${f.consumed_ml} ml ${f.milk_type}` : `${f.solid_g} g ${f.item}` }}
+ · {{ f.note }}
+
+
+
+
+
+
+
+
Keine Mahlzeiten verzeichnet.
+
+
+
+
+
+
+ Windeln ({{ selectedDayDetail.diapers.length }})
+
+
+
+
+
+ {{ d.time }} Uhr: {{ d.pee && d.poo ? 'Pipi + Kacka' : (d.poo ? 'Nur Kacka' : 'Nur Pipi') }}
+
+
+ Konsistenz: {{ d.texture }}
+ · {{ d.note }}
+
+
+
+
+
+
+
+
Keine Windelwechsel verzeichnet.
+
+
+
+
+
+
+
+
+
+
+
+
Mila zum Home-Bildschirm
+
+
✕
+
+
+ So installierst du die Mila-App auf deinem iPhone oder iPad:
+
+
+ Tippe in Safari unten auf das Teilen-Symbol (Quadrat mit Pfeil nach oben).
+ Scrolle im Menü nach unten und wähle „Zum Home-Bildschirm“ (+).
+ Tippe oben rechts auf „Hinzufügen“ .
+
+
+ Verstanden
+
+
+
+
+
diff --git a/projects/mila/src/api.ts b/projects/mila/src/api.ts
new file mode 100644
index 0000000..4540b14
--- /dev/null
+++ b/projects/mila/src/api.ts
@@ -0,0 +1,84 @@
+import type { MilaDashboardData, DayDetailData } from './types'
+
+const API_BASE = '/duckdb-api/v1'
+
+export function getLocalDateString(): string {
+ const d = new Date()
+ const year = d.getFullYear()
+ const month = String(d.getMonth() + 1).padStart(2, '0')
+ const day = String(d.getDate()).padStart(2, '0')
+ return `${year}-${month}-${day}`
+}
+
+export async function fetchMilaDashboard(asOf?: string): Promise {
+ const url = asOf ? `${API_BASE}/mila/dashboard?as_of=${encodeURIComponent(asOf)}` : `${API_BASE}/mila/dashboard`
+ const res = await fetch(url)
+ if (!res.ok) {
+ throw new Error(`Failed to fetch dashboard: ${res.status} ${res.statusText}`)
+ }
+ return res.json()
+}
+
+export async function fetchMilaDayDetail(date: string): Promise {
+ const res = await fetch(`${API_BASE}/mila/day/${encodeURIComponent(date)}`)
+ if (!res.ok) {
+ throw new Error(`Failed to fetch day detail for ${date}: ${res.statusText}`)
+ }
+ return res.json()
+}
+
+export async function logEvent(payload: {
+ kind: string
+ subject: string
+ payload: Record
+ start?: string
+ end?: string
+ date?: string
+ since?: string
+ note?: string
+ actor?: string
+}): Promise {
+ const res = await fetch(`${API_BASE}/events`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(payload),
+ })
+ if (!res.ok) {
+ const err = await res.json().catch(() => ({ detail: res.statusText }))
+ throw new Error(err.detail || 'Failed to log event')
+ }
+ return res.json()
+}
+
+export async function stopSleepEvent(payload: {
+ subject: string
+ end_time?: string
+ since?: string
+ event_id?: string
+}): Promise {
+ const res = await fetch(`${API_BASE}/events/stop`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ kind: 'sleep.session',
+ ...payload,
+ }),
+ })
+ if (!res.ok) {
+ const err = await res.json().catch(() => ({ detail: res.statusText }))
+ throw new Error(err.detail || 'Failed to stop sleep event')
+ }
+ return res.json()
+}
+
+export async function deleteEvent(eventId: string): Promise {
+ const res = await fetch(`${API_BASE}/events/${encodeURIComponent(eventId)}`, {
+ method: 'DELETE',
+ })
+ if (!res.ok) {
+ const err = await res.json().catch(() => ({ detail: res.statusText }))
+ throw new Error(err.detail || 'Failed to delete event')
+ }
+ return res.json()
+}
+
diff --git a/projects/mila/src/components/FeedingAnalyticsTool.vue b/projects/mila/src/components/FeedingAnalyticsTool.vue
new file mode 100644
index 0000000..fefa532
--- /dev/null
+++ b/projects/mila/src/components/FeedingAnalyticsTool.vue
@@ -0,0 +1,183 @@
+
+
+
+
+
+
+
+
+
+
+
+
Ernährung & Beikost-Wissenschaft
+
Milch-zu-Brei Transition nach DGKJ & FKE Dortmund
+
+
+
+
+
+
+
+ {{ todayFeed.total_consumed_ml }} ml
+
+
+
+ {{ todayFeed.total_solid_g }} g
+
+
+
+
+
+
+
+
+
+
+
+ Tages-Energie
+
+ ~{{ todayEstimatedKcal }} / {{ targetKcal }} kcal
+
+
+
+ EFSA-Empfehlung: ca. 80–85 kcal/kg Körpergewicht (~620–680 kcal/Tag bei ~7.4 kg).
+
+
+
+
+
+
Milch-Schnitt (7T)
+
+
+ Sichert die Grundversorgung mit Flüssigkeit, Eiweiß und Kalzium im Beikost-Alter.
+
+
+
+
+
+
Brei-Schnitt (7T)
+
+
+ Fördert Kau- und Schluckmotorik sowie Nährstoffdichte (Eisen, Zink).
+
+
+
+
+
+
+
+
14-Tage Verlauf: Milch (ml) vs. Brei (g)
+
+
+ Milch (ml / 10)
+
+
+ Brei (g)
+
+
+
+
+
+
+
+
+ {{ d.weekday }} {{ d.date.slice(5) }}: {{ d.feed_bottles_ml }}ml / {{ d.feed_solid_g }}g
+
+
+
+
+
+ {{ d.date.slice(8) }}
+
+
+
+
+
+
+
+
+
+ FKE Dortmund: Ernährungsplan im 1. Lebensjahr
+
+
+
+
① Mittag: Gemüse-Kartoffel-Fleisch
+
+ Liefert wichtiges Eisen und Zink zur Blutbildung und Hirnentwicklung nach Aufbrauchen der pränatalen Speicher.
+
+
+
+
② Abend: Milch-Getreide-Brei
+
+ Kombiniert Kalzium aus Vollmilch mit komplexen Kohlenhydraten (Hafer/Grieß) für langanhaltende Sättigung in der Nacht.
+
+
+
+
③ Nachmittag: Getreide-Obst-Brei
+
+ Milchfreier Brei; Vitamin C aus Früchten optimiert die pflanzliche Eisenresorption und liefert milde Ballaststoffe.
+
+
+
+
+
+
diff --git a/projects/mila/src/components/SleepChart.vue b/projects/mila/src/components/SleepChart.vue
new file mode 100644
index 0000000..3aedad6
--- /dev/null
+++ b/projects/mila/src/components/SleepChart.vue
@@ -0,0 +1,589 @@
+
+
+
+
+
+
+
+
+
+
+
+
Schlafarchitektur & Wissenschaft
+
Trend, 24h-Hypnogramm & pädiatrische Normen
+
+
+
+
+
+
+
+
+ Trend
+
+
+
+ 24h-Muster
+
+
+
+ Normen
+
+
+
+
+
+
+ {{ p === '7d' ? '7T' : p === '14d' ? '14T' : '30T' }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ activeBar.weekday }}, {{ activeBar.formatted_date }}
+
+
+ Heute
+
+
+
+
+
+ Gesamt:
+ {{ activeBar.totalHours.toFixed(1) }} h
+
+
+ 🌙
+ {{ activeBar.nightHours.toFixed(1) }}h
+
+
+ 🌿
+ {{ activeBar.napHours.toFixed(1) }}h
+
+
+ Ø7T:
+ {{ activeBar.avg7.toFixed(1) }}h
+
+
+ {{ activeBar.isWithinWho ? 'WHO Norm (12–15h)' : (activeBar.isBelowWho ? '< 12h' : '> 15h') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ WHO Richtwert (12–15h)
+
+
+
+
+ 18h
+ 15h
+ 12h
+ 6h
+ 0h
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ bar.weekday }}
+
+
+ {{ bar.date.slice(8) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Nacht
+
+
+
+ Naps
+
+
+
+ 7T-Schnitt
+
+
+
+ WHO (12–15h)
+
+
+
+
+ Ø Gesamt: {{ summary.avgTotal }}h
+ Norm: {{ summary.whoPercent }}%
+
+
+
+
+
+
+
+
Täglicher Schlafverlauf von 00:00 bis 24:00 Uhr
+
+
+ Nacht
+
+
+ Tagesschläfchen
+
+
+
+
+
+
+
00:00
+
06:00
+
12:00
+
18:00
+
24:00
+
+
+
+
+
+
+
+ {{ day.weekday }}
+ {{ day.date.slice(5) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ s.duration_minutes }}m
+
+
+
+
+
Keine Einzelschläfchen
+
+
+
+
+
+
+
+
+
+
+
+ 24h Gesamtschlaf
+ {{ summary.avgTotal }}h / 12–15h
+
+
+
+
+ WHO / AASM Richtwert (12–15h): Unterstützt die kognitive Konsolidierung, das Längenwachstum durch Wachstumshormone im Tiefschlaf und beugt Übermüdung vor.
+
+
+
+
+
+
+ Nachtschlaf
+ {{ summary.avgNight }}h / 10–11.5h
+
+
+
+ Remo H. Largo (Babyjahre): Im 2. Lebenshalbjahr konsolidiert sich der Nachtschlaf auf ca. 10–11 Stunden am Stück (mit altersgemäßen Aufwachphasen zur Nahrung/Rückversicherung).
+
+
+
+
+
+
+ Nap-Frequenz
+ {{ summary.napCountAvg }} Naps / 2–3 Naps
+
+
+ Entwicklungsübergang (6–9 Monate): Die meisten Babys wechseln von 3 kurzen Nickerchen zu 2 stabilen Schläfchen (Vormittag + Nachmittag). Mila liegt mit Ø {{ summary.napCountAvg }} Naps voll im Normbereich.
+
+
+
+
+
+
+ Sweet-Spot Wachfenster
+ 2h 15m – 3h 00m
+
+
+ Schlafdruck vs. Übermüdung: Nach 2h 15m baut sich der ideale Schlafdruck auf. Ab 3h 15m steigt der Cortisol- und Adrenalinspiegel, was das Einschlafen erschwert.
+
+
+
+
+
+
diff --git a/projects/mila/src/components/TrackingModal.vue b/projects/mila/src/components/TrackingModal.vue
new file mode 100644
index 0000000..9e8fbf1
--- /dev/null
+++ b/projects/mila/src/components/TrackingModal.vue
@@ -0,0 +1,579 @@
+
+
+
+
+
+
+
+
+
+ ✨
+
Ereignis erfassen
+
+
+
+
+
+
+
+
+
+
+ Schlaf
+
+
+
+ Nahrung
+
+
+
+ Windel
+
+
+
+ Wachstum
+
+
+
+
+
+
+ 📅
+ Ereignis-Datum:
+
+
+
+
+
+
+ {{ errorMessage }}
+
+
+
+ {{ successMessage }}
+
+
+
+
+
+
+
+
+
+
1-Tap Sofort-Logging
+
+
+
+ Schläfchen JETZT starten
+
+
+ 🌙 Nachtschlaf starten
+
+
+
+
+
+ Schlaf JETZT beenden (Aufgewacht!)
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 🍼 Fläschchen
+
+
+ 🥣 Brei / Beikost
+
+
+
+
+
+
+
Schnellwahl (ml)
+
+
+ {{ ml }}
+
+
+
+
+
+ Getrunkene Menge (ml)
+
+
+
+ Milchtyp
+
+ Pre
+ Muttermilch
+ 1er Nahrung
+
+
+
+
+ Uhrzeit (leer = Jetzt)
+
+
+
+ Flasche speichern
+
+
+
+
+
+
+ Brei-Bezeichnung
+
+
+
+
Schnellwahl Gramm
+
+
+ {{ g }}g
+
+
+
+
+
+ Brei speichern
+
+
+
+
+
+
+
1-Tap Windelwechsel
+
+
+ 💧
+ Nur Pipi
+
+
+ 💩
+ Nur Kacka
+
+
+ 🧷
+ Beides
+
+
+
+
+
+ Konsistenz (optional)
+
+ Weich
+ Breiig
+ Fest
+ Flüssig
+
+
+
+ Notiz
+
+
+
+
+
+
+
+
+
+ Notiz
+
+
+
+ Messung speichern
+
+
+
+
+
+
+
diff --git a/projects/mila/src/components/WhoGrowthTool.vue b/projects/mila/src/components/WhoGrowthTool.vue
new file mode 100644
index 0000000..6482059
--- /dev/null
+++ b/projects/mila/src/components/WhoGrowthTool.vue
@@ -0,0 +1,378 @@
+
+
+
+
+
+
+
+
+
+
+
+
WHO Wachstums- & Perzentilenkurven
+
Offizielle WHO-Standards für Mädchen (0–18 Monate)
+
+
+
+
+
+
+ ⚖️ Gewicht (kg)
+
+
+ 📏 Körpergröße (cm)
+
+
+
+
+
+
+
+
+
+ Alter: {{ inspectedData.month }} Monate
+
+ · WHO P50 Median:
+ {{ inspectedData.p50 }} {{ inspectedData.unit }}
+
+
+
+
+ Normalbereich (P15–P85):
+ {{ inspectedData.p15 }} – {{ inspectedData.p85 }} {{ inspectedData.unit }}
+
+
+
+ Mila: {{ inspectedData.milaPt.val }} {{ inspectedData.unit }} ({{ inspectedData.milaPt.pctStr }})
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 14kg
+ 10kg
+ 6kg
+ 2kg
+
+
+ 85cm
+ 70cm
+ 55cm
+
+
+
+
+
+
+
+ {{ m }}m
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ P50 (Median)
+
+
+
+ P15–P85 (Normalspanne)
+
+
+
+ Mila Messpunkt
+
+
+
+
+ WHO Child Growth Standards (WHO MGRS)
+
+
+
+
diff --git a/projects/mila/src/main.ts b/projects/mila/src/main.ts
new file mode 100644
index 0000000..8ec0ac9
--- /dev/null
+++ b/projects/mila/src/main.ts
@@ -0,0 +1,30 @@
+///
+import { createApp } from 'vue'
+import './style.css'
+import App from './App.vue'
+
+// Client-side canonical path enforcement
+if (typeof window !== 'undefined' && (window.location.pathname === '' || window.location.pathname === '/')) {
+ window.location.replace('/tracking/mila/')
+}
+
+createApp(App).mount('#app')
+
+// Reliable Service Worker registration (catches readyState complete and load)
+if ('serviceWorker' in navigator) {
+ const registerSW = () => {
+ navigator.serviceWorker
+ .register('/tracking/mila/sw.js', { scope: '/tracking/mila/' })
+ .then((reg) => {
+ // Check for updates
+ reg.update().catch(() => {})
+ })
+ .catch((err) => console.warn('Mila Service Worker registration failed:', err))
+ }
+
+ if (document.readyState === 'complete') {
+ registerSW()
+ } else {
+ window.addEventListener('load', registerSW)
+ }
+}
diff --git a/projects/mila/src/style.css b/projects/mila/src/style.css
new file mode 100644
index 0000000..b55b8e1
--- /dev/null
+++ b/projects/mila/src/style.css
@@ -0,0 +1,16 @@
+@import "tailwindcss";
+
+@layer base {
+ body {
+ font-family: 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
+ -webkit-tap-highlight-color: transparent;
+ }
+
+ .font-heading {
+ font-family: 'Syne', sans-serif;
+ }
+
+ .font-mono {
+ font-family: 'JetBrains Mono', monospace;
+ }
+}
diff --git a/projects/mila/src/types.ts b/projects/mila/src/types.ts
new file mode 100644
index 0000000..11ce58b
--- /dev/null
+++ b/projects/mila/src/types.ts
@@ -0,0 +1,159 @@
+export interface NapSession {
+ id: string
+ start: string
+ end: string
+ sleep_type: string
+ location: string
+ note: string
+ duration_minutes: number | null
+ is_open: boolean
+}
+
+export interface FeedingItem {
+ id?: string
+ time: string
+ consumed_ml: number | null
+ milk_type: string
+ item: string
+ solid_g: number | null
+ kcal: number | null
+ kind: string
+ note: string
+}
+
+export interface DiaperItem {
+ id?: string
+ time: string
+ pee: boolean
+ poo: boolean
+ texture: string
+ note: string
+}
+
+export interface HistoryRow {
+ date: string
+ weekday: string
+ formatted_date: string
+ is_today: boolean
+ total_hours: number
+ night_duration_str: string
+ nap_str: string
+ feed_bottles_ml: number
+ feed_bottles_count: number
+ feed_solid_g: number
+ feed_solid_count: number
+ diaper_changes: number
+ diaper_dirty: number
+ sessions: {
+ id?: string
+ start: string
+ end: string
+ type: string
+ duration_minutes: number | null
+ is_open: boolean
+ note: string
+ location?: string
+ }[]
+}
+
+export interface GrowthMeasurement {
+ id: string
+ date_str: string
+ date_pretty: string
+ age_str: string
+ age_months: number
+ weight_kg: number | null
+ weight_pct: string
+ height_cm: number | null
+ height_pct: string
+ head_circ_cm: number | null
+ note: string
+}
+
+export interface DayDetailData {
+ date: string
+ weekday: string
+ sleep_sessions: NapSession[]
+ feedings: FeedingItem[]
+ diapers: DiaperItem[]
+ growth?: GrowthMeasurement[]
+}
+
+export interface AgeNorms {
+ category: string
+ sweet_spot_wake_window_min: [number, number]
+ total_sleep_norm_hours: [number, number]
+ night_sleep_norm_hours: [number, number]
+ nap_count_norm: [number, number]
+ feed_interval_hours: [number, number]
+ guidelines_reference: string
+}
+
+export interface MilaDashboardData {
+ status: string
+ execution_time_ms: number
+ git_status: string
+ git_conflict: boolean
+ metadata: {
+ subject: string
+ today_date: string
+ today_pretty: string
+ age_str: string
+ }
+ live_status: {
+ is_sleeping: boolean
+ active_sleep_id: string | null
+ active_sleep_duration_str: string
+ active_sleep_start_time: string
+ active_sleep_iso: string
+ wake_window_minutes: number | null
+ wake_window_str: string
+ last_wake_time_str: string
+ last_wake_iso: string
+ last_feed_time: string | null
+ last_feed_elapsed_minutes: number | null
+ last_feed_item: string | null
+ last_feed_amount: number | null
+ last_diaper_time: string | null
+ last_diaper_elapsed_minutes: number | null
+ last_stool_time: string | null
+ last_stool_elapsed_minutes: number | null
+ }
+ age_norms?: AgeNorms
+ today: {
+ total_sleep_hours_str: string
+ nap_count: number
+ nap_minutes: number
+ night_duration_str: string
+ naps: NapSession[]
+ nights: NapSession[]
+ feedings: FeedingItem[]
+ feeding_summary: {
+ bottle_count: number
+ total_consumed_ml: number
+ solid_count: number
+ total_solid_g: number
+ total_kcal: number
+ }
+ diapers: DiaperItem[]
+ diaper_summary: {
+ total_changes: number
+ wet_count: number
+ dirty_count: number
+ }
+ }
+ averages: {
+ avg_7_tot: string
+ avg_7_night: string
+ avg_7_naps: string
+ avg_30_tot: string
+ avg_7_feeding_ml?: number
+ avg_7_solid_g?: number
+ avg_7_diapers?: number
+ }
+ history: HistoryRow[]
+ growth: {
+ measurements: GrowthMeasurement[]
+ latest: GrowthMeasurement | null
+ }
+}
diff --git a/projects/mila/tsconfig.json b/projects/mila/tsconfig.json
new file mode 100644
index 0000000..395c931
--- /dev/null
+++ b/projects/mila/tsconfig.json
@@ -0,0 +1,20 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "module": "ESNext",
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "skipLibCheck": true,
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "isolatedModules": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+ "jsx": "preserve",
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
+}
diff --git a/projects/mila/vite.config.ts b/projects/mila/vite.config.ts
new file mode 100644
index 0000000..a25c7f7
--- /dev/null
+++ b/projects/mila/vite.config.ts
@@ -0,0 +1,20 @@
+import { defineConfig } from 'vite'
+import vue from '@vitejs/plugin-vue'
+import tailwindcss from '@tailwindcss/vite'
+
+export default defineConfig({
+ base: '/tracking/mila/',
+ plugins: [
+ vue(),
+ tailwindcss(),
+ ],
+ server: {
+ port: 5173,
+ proxy: {
+ '/duckdb-api': {
+ target: 'http://localhost:8000',
+ changeOrigin: true,
+ },
+ },
+ },
+})
diff --git a/projects/moritz/AGENTS.md b/projects/moritz/AGENTS.md
new file mode 100644
index 0000000..2cf01d5
--- /dev/null
+++ b/projects/moritz/AGENTS.md
@@ -0,0 +1,9 @@
+# AGENTS.md — Moritz Tracking App
+
+## Development
+```bash
+cd projects/moritz
+npm install
+npm run dev
+npm run build
+```
diff --git a/projects/moritz/Dockerfile b/projects/moritz/Dockerfile
new file mode 100644
index 0000000..5920d82
--- /dev/null
+++ b/projects/moritz/Dockerfile
@@ -0,0 +1,13 @@
+FROM node:20-alpine AS build
+WORKDIR /app
+COPY package*.json ./
+RUN npm install
+COPY . .
+RUN npm run build
+
+FROM nginx:alpine
+COPY nginx.conf /etc/nginx/conf.d/default.conf
+COPY --from=build /app/dist/ /usr/share/nginx/html/
+RUN mkdir -p /usr/share/nginx/html/tracking && ln -s /usr/share/nginx/html /usr/share/nginx/html/tracking/moritz
+EXPOSE 80
+CMD ["nginx", "-g", "daemon off;"]
diff --git a/projects/moritz/README.md b/projects/moritz/README.md
new file mode 100644
index 0000000..9a20d29
--- /dev/null
+++ b/projects/moritz/README.md
@@ -0,0 +1,13 @@
+# Moritz Tracking App
+
+Apple Health-styled personal energy balance and Garmin physiological metrics dashboard.
+Built with Vue 3 (Composition API), Vite, TypeScript, and Tailwind CSS.
+
+## Features
+- **Hero Netto-Bilanz**: 24h caloric surplus/deficit calculated by DuckDB (Intake vs. Garmin Burn).
+- **Today's Intake**: Calories, macros (protein, carbs, fat), and real-time meal counter.
+- **Garmin Recovery & Sleep**: Sleep score, HRV, resting heart rate, body battery, sleep stages.
+- **7-Day Trend**: Daily comparative bars and rolling averages.
+- **30-Day History**: Monthly summary and day-by-day table.
+- **Tracking Modal**: 1-tap quick favorite meals + custom detailed logging.
+- **Backend**: Direct sub-30ms analytical queries to FastAPI DuckDB backend at `/duckdb-api/v1`.
diff --git a/projects/moritz/index.html b/projects/moritz/index.html
new file mode 100644
index 0000000..b094f9c
--- /dev/null
+++ b/projects/moritz/index.html
@@ -0,0 +1,37 @@
+
+
+
+
+
+ Moritz Health — Energiebilanz & Sport
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/projects/moritz/nginx.conf b/projects/moritz/nginx.conf
new file mode 100644
index 0000000..84dac41
--- /dev/null
+++ b/projects/moritz/nginx.conf
@@ -0,0 +1,53 @@
+server {
+ listen 80;
+ server_name _;
+ absolute_redirect off;
+ port_in_redirect off;
+
+ root /usr/share/nginx/html;
+ index index.html;
+
+ include /etc/nginx/mime.types;
+ types {
+ application/manifest+json webmanifest;
+ }
+
+ location = /tracking/moritz {
+ return 301 /tracking/moritz/;
+ }
+
+ location /tracking/moritz/ {
+ try_files $uri $uri/ /tracking/moritz/index.html /index.html;
+ }
+
+ location / {
+ try_files $uri $uri/ /index.html;
+ }
+
+ # Service worker
+ location ~* /sw\.js$ {
+ default_type application/javascript;
+ expires -1;
+ add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
+ add_header Service-Worker-Allowed "/tracking/moritz/";
+ }
+
+ # Webmanifest and manifest.json
+ location ~* /(?:moritz\.webmanifest|manifest\.webmanifest|manifest\.json)$ {
+ default_type application/manifest+json;
+ expires -1;
+ add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
+ }
+
+ # Static assets cache
+ location ~* \.(?:css|js|jpg|jpeg|gif|png|ico|svg|woff|woff2|ttf)$ {
+ expires 1y;
+ add_header Cache-Control "public";
+ }
+
+ error_page 500 502 503 504 /50x.html;
+ location = /50x.html {
+ root /usr/share/nginx/html;
+ }
+}
+
diff --git a/projects/moritz/package-lock.json b/projects/moritz/package-lock.json
new file mode 100644
index 0000000..59e7cb7
--- /dev/null
+++ b/projects/moritz/package-lock.json
@@ -0,0 +1,2251 @@
+{
+ "name": "moritz-tracking",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "moritz-tracking",
+ "version": "1.0.0",
+ "dependencies": {
+ "lucide-vue-next": "^0.468.0",
+ "vue": "^3.5.13"
+ },
+ "devDependencies": {
+ "@tailwindcss/vite": "^4.0.9",
+ "@types/node": "^22.10.2",
+ "@vitejs/plugin-vue": "^5.2.1",
+ "tailwindcss": "^4.0.9",
+ "typescript": "^5.7.2",
+ "vite": "^6.2.0",
+ "vue-tsc": "^2.2.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
+ "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
+ "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.9",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.9.tgz",
+ "integrity": "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.8"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.8",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz",
+ "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.29.7",
+ "@babel/helper-validator-identifier": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
+ "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
+ "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
+ "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
+ "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
+ "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
+ "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
+ "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
+ "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
+ "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
+ "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
+ "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
+ "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
+ "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
+ "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
+ "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
+ "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
+ "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
+ "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
+ "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
+ "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
+ "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz",
+ "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==",
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@napi-rs/lzma-linux-x64-gnu": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz",
+ "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^22.20 || ^24.12 || >=25"
+ }
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.64.6",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.64.6.tgz",
+ "integrity": "sha512-FkO19Fowv/OQeJZPVfdetJaI3/VpbZmBsnmAlR/rQ+b3QwfiGQiOpVegpSRH331kelvHTVw9hXYG3DwjAaRzyQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.64.6",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.64.6.tgz",
+ "integrity": "sha512-si5/Etm6MphPc2xYHnk455Shls/hoMjTy3mhXge4OPmvCw7TwC4KP4fKokAlKLV4XaQvLvgSHfafPcblLMYYdQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.64.6",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.64.6.tgz",
+ "integrity": "sha512-PQCl3q7b0BgvVt72i2xrWM6I5KliwwVe4GpWW5QDGyUQawDS9aPZss/aGyFqBmxdRxlE8wDc9CaxiQ5DHRSp/Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.64.6",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.64.6.tgz",
+ "integrity": "sha512-l9DFl1Itb1YYdSrP1K3U8ih44AOHa/p+Q4WMMDfGvH5qVWp+ajfgS1DlYo4yCRh8wJ2ZJv0Fqccf+ULAmMFjmg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.64.6",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.64.6.tgz",
+ "integrity": "sha512-bR4GR3205N9+od0+l5Dd9/iZTDpJkIe6d6Ydp2qbcQZJLmKBdYQQRp8gbWpbMt92twKJY66o1vK4NkoKQODIrA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.64.6",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.64.6.tgz",
+ "integrity": "sha512-XTVNaDzcFl2TeBglvxdGgsCv9JGtAGRAoJENmGBR/XvDH2mA2K0IXc2aMfP0dR8mexSXSKifJ85FixoXX1wfTw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.64.6",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.64.6.tgz",
+ "integrity": "sha512-qWc6qbc+25ugG9vqPR9P422KJBbVfZ05mjcMggoq8tsfWP+eRfC+/tiDdIlRF5VL2pDVZWN9zAhWsmu7R1+7Dw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.64.6",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.64.6.tgz",
+ "integrity": "sha512-V4Nwkp8844X/Xnww3PI1LYS0YPei8XQEVG70R+5MPZGO7AhYoun4hCN7Svs4U3wbQLZbwiz+9xRFhUZZdRl8Ww==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.64.6",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.64.6.tgz",
+ "integrity": "sha512-xb72A1aPaaIsp+a88VtRae/ovIEXislbUMyp7b9AQQU7ZkP2m/+ik6DW9TQcJyw5R8/RIailF9zExYwkjR++UA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.64.6",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.64.6.tgz",
+ "integrity": "sha512-pbcs1BVBwC43hZkjhjlF8Rx8m5G0KnhLZs9Azy/gqccP6UsNSzJr0hKGIAVPqzAH1O/NdPxxC9GVGb4msQg/aw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.64.6",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.64.6.tgz",
+ "integrity": "sha512-EKOC8os6Vsg487ZLKUqzLF99Jsmnq6tu7O7mAELakNpcA+8KadktFf/JYdRuHe/DzCUXmN38pUKu4XBcKlqGbQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.64.6",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.64.6.tgz",
+ "integrity": "sha512-v/SQ68GnSlr/HyjB79sNXesGDwkOPu9JjbHfmWaymCiM+nZsQ9TstG1KfA4MuEvTr2d3PoY8PhFxj9bt0e32JA==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.64.6",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.64.6.tgz",
+ "integrity": "sha512-G3pmliMQ7r15zFFNwBlaT/xTdSLi4lfgI7CUrQueyKYheNVhrd8JrWCE6Ll8xf4AA1RZFqq+mNJYntiElsVOyA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.64.6",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.64.6.tgz",
+ "integrity": "sha512-XQTB/IPoB/wG6x/wu9uomAt8YH61IAr5iZTubMiad/lu88sotq2ocz9hJd556+Jz2kIkF7ZQZIsUuWE3OWqtcA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.64.6",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.64.6.tgz",
+ "integrity": "sha512-O5mpokth6+ydMuwiGtY8fIjvOBfH7u6SzmDNmZ3k6GK5C6iEAC2q2ZN/T5L9jYILx0IcCT0KnfMDihQ5yQFSBw==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.64.6",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.64.6.tgz",
+ "integrity": "sha512-aIFxbAD9v3TvIaEDsKmukjcDg1JS4/tFXw8SHyQQgzOwzWCQ4cyDK7Ju1pQKqUXDFkEu/BZCFDCDATsKhQcB/Q==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.64.6",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.64.6.tgz",
+ "integrity": "sha512-fuNo1UwBc6BxnHDOxxguQL/InhheMN3RJcJMb0b4xP8r5Q3CNCrJbVg0eCIFuJ4e/zAfjR2gYbOfbzO/G7CIyw==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.64.6",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.64.6.tgz",
+ "integrity": "sha512-05v6wFKDKJPZs9xlytVrrRu6IbRuoSvlH9xNwEOafXsKyjBFZPvAcZz2VIAB/M+lblYeKa9iOYC4RDVdfcAAaw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.64.6",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.64.6.tgz",
+ "integrity": "sha512-wJXKcdMjvkc/FaavZXNP1yW4i0pfOmhZAIWRSLG2yJ0PB0TWi7dkLErhX0u0vptDcXKKO/m1Y/YTeuyvb2MXwg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.64.6",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.64.6.tgz",
+ "integrity": "sha512-cwg7/h0ThvJwy8CBkmACdiHAxRWpbsCmIsjOt+Zzx0hTnIcSUmKYEV03bGg+hD827OnwUsbKh1ZZDW754xWyBA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.64.6",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.64.6.tgz",
+ "integrity": "sha512-T0RkYIJbdYXJEQFw+tMBO09KDFdgmNxkHsof3wWAABrvnx15Yh8RytmrgYy9+q9cBeBNZF66MMQZC+5nzEz6vg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.64.6",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.64.6.tgz",
+ "integrity": "sha512-KlfpmF1AAIdAWxt8QQNClbpLFlAUfYOzqIuJ7IXEStMHpFRuH78RG6FjqPb3Rqhh0K89F5npvG+/i3Fjl6IveQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.64.6",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.64.6.tgz",
+ "integrity": "sha512-MrTLjd7Jlt9/ZavJZYG61KQv9Iy9bzbY+yXy73Xvwoa2uP66wvDShkyBr5Chz/yrE6lbD/zHMuTmKQVNQqBApg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.64.6",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.64.6.tgz",
+ "integrity": "sha512-8fWDH1CkPI8GdBKAxzHDza7UmgficsKo2+MVbvUgOGhB0629w5sHMg3Qu5y7TZsrYKHHWpz8fla4KvoT7WsO3Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.64.6",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.64.6.tgz",
+ "integrity": "sha512-9/c9PWdr5GCIHkkxiHM1HjNBQMVdftP4O8QTcoUAaRza2FSqJ4axbf8QAnBuE6BIGQDl71SaRPeDdDYid5ecTw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@tailwindcss/node": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz",
+ "integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/remapping": "^2.3.5",
+ "enhanced-resolve": "^5.24.1",
+ "jiti": "^2.7.0",
+ "lightningcss": "1.32.0",
+ "magic-string": "^0.30.21",
+ "source-map-js": "^1.2.1",
+ "tailwindcss": "4.3.3"
+ }
+ },
+ "node_modules/@tailwindcss/oxide": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz",
+ "integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 20"
+ },
+ "optionalDependencies": {
+ "@tailwindcss/oxide-android-arm64": "4.3.3",
+ "@tailwindcss/oxide-darwin-arm64": "4.3.3",
+ "@tailwindcss/oxide-darwin-x64": "4.3.3",
+ "@tailwindcss/oxide-freebsd-x64": "4.3.3",
+ "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3",
+ "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3",
+ "@tailwindcss/oxide-linux-arm64-musl": "4.3.3",
+ "@tailwindcss/oxide-linux-x64-gnu": "4.3.3",
+ "@tailwindcss/oxide-linux-x64-musl": "4.3.3",
+ "@tailwindcss/oxide-wasm32-wasi": "4.3.3",
+ "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3",
+ "@tailwindcss/oxide-win32-x64-msvc": "4.3.3"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-android-arm64": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.3.tgz",
+ "integrity": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-darwin-arm64": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.3.tgz",
+ "integrity": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-darwin-x64": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.3.tgz",
+ "integrity": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-freebsd-x64": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.3.tgz",
+ "integrity": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.3.tgz",
+ "integrity": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.3.tgz",
+ "integrity": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm64-musl": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.3.tgz",
+ "integrity": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-x64-gnu": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.3.tgz",
+ "integrity": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-x64-musl": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.3.tgz",
+ "integrity": "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.3.tgz",
+ "integrity": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==",
+ "bundleDependencies": [
+ "@napi-rs/wasm-runtime",
+ "@emnapi/core",
+ "@emnapi/runtime",
+ "@tybys/wasm-util",
+ "@emnapi/wasi-threads",
+ "tslib"
+ ],
+ "cpu": [
+ "wasm32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "^1.11.1",
+ "@emnapi/runtime": "^1.11.1",
+ "@emnapi/wasi-threads": "^1.2.2",
+ "@napi-rs/wasm-runtime": "^1.1.4",
+ "@tybys/wasm-util": "^0.10.2",
+ "tslib": "^2.8.1"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz",
+ "integrity": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-win32-x64-msvc": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.3.tgz",
+ "integrity": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/vite": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.3.tgz",
+ "integrity": "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@tailwindcss/node": "4.3.3",
+ "@tailwindcss/oxide": "4.3.3",
+ "tailwindcss": "4.3.3"
+ },
+ "peerDependencies": {
+ "vite": "^5.2.0 || ^6 || ^7 || ^8"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
+ "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "22.20.5",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.5.tgz",
+ "integrity": "sha512-U2+DNr+wSjpsTS/wZGYHq7GcwfuSmKiKvoPvK22zwTlRhU91yOniN4qRR5KhIjvif7ysw/dz/hKmfDH0Ris4aA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~6.21.0"
+ }
+ },
+ "node_modules/@vitejs/plugin-vue": {
+ "version": "5.2.4",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz",
+ "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "peerDependencies": {
+ "vite": "^5.0.0 || ^6.0.0",
+ "vue": "^3.2.25"
+ }
+ },
+ "node_modules/@volar/language-core": {
+ "version": "2.4.15",
+ "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.15.tgz",
+ "integrity": "sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@volar/source-map": "2.4.15"
+ }
+ },
+ "node_modules/@volar/source-map": {
+ "version": "2.4.15",
+ "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.15.tgz",
+ "integrity": "sha512-CPbMWlUN6hVZJYGcU/GSoHu4EnCHiLaXI9n8c9la6RaI9W5JHX+NqG+GSQcB0JdC2FIBLdZJwGsfKyBB71VlTg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@volar/typescript": {
+ "version": "2.4.15",
+ "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.15.tgz",
+ "integrity": "sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@volar/language-core": "2.4.15",
+ "path-browserify": "^1.0.1",
+ "vscode-uri": "^3.0.8"
+ }
+ },
+ "node_modules/@vue/compiler-core": {
+ "version": "3.5.44",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.44.tgz",
+ "integrity": "sha512-k34nCytbK8PQ1gR2mGROYOw6Pnv/mumqIdrZ3x2tbyXZDWm7ViKGLTfdB+DRrWqEQsLgGJvn0WN9jBif5zCTLQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.9",
+ "@vue/shared": "3.5.44",
+ "entities": "^7.0.1",
+ "estree-walker": "^2.0.2",
+ "source-map-js": "^1.2.2"
+ }
+ },
+ "node_modules/@vue/compiler-dom": {
+ "version": "3.5.44",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.44.tgz",
+ "integrity": "sha512-W5AoBFWdw1lQAKiHowkfgFKFab82oSRT6sGsPfw/q1GRjsy/ZJFLHGhJyBRIFN9APLTUduDH9U2JAiPOIyKuYg==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-core": "3.5.44",
+ "@vue/shared": "3.5.44"
+ }
+ },
+ "node_modules/@vue/compiler-sfc": {
+ "version": "3.5.44",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.44.tgz",
+ "integrity": "sha512-dzHmQ4jXTpdr5FjIQUeykVakN/uzqFbUo9/C5ikBFg3a3PeM6uRxeczx1ZbXBML0/zJstEh3ygYipzR5Z7b2Pw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.9",
+ "@vue/compiler-core": "3.5.44",
+ "@vue/compiler-dom": "3.5.44",
+ "@vue/compiler-ssr": "3.5.44",
+ "@vue/shared": "3.5.44",
+ "estree-walker": "^2.0.2",
+ "magic-string": "^0.30.21",
+ "postcss": "^8.5.29",
+ "source-map-js": "^1.2.2"
+ }
+ },
+ "node_modules/@vue/compiler-ssr": {
+ "version": "3.5.44",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.44.tgz",
+ "integrity": "sha512-xr5gb1JVHlfXJwSlRz1FeP6uxoazdzHuMTVLE7up+4Vrqxk219CvxLPxD9NXBt47yvZW7+p4DXfyp4O1fkByIA==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-dom": "3.5.44",
+ "@vue/shared": "3.5.44"
+ }
+ },
+ "node_modules/@vue/compiler-vue2": {
+ "version": "2.7.16",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz",
+ "integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "de-indent": "^1.0.2",
+ "he": "^1.2.0"
+ }
+ },
+ "node_modules/@vue/language-core": {
+ "version": "2.2.12",
+ "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.2.12.tgz",
+ "integrity": "sha512-IsGljWbKGU1MZpBPN+BvPAdr55YPkj2nB/TBNGNC32Vy2qLG25DYu/NBN2vNtZqdRbTRjaoYrahLrToim2NanA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@volar/language-core": "2.4.15",
+ "@vue/compiler-dom": "^3.5.0",
+ "@vue/compiler-vue2": "^2.7.16",
+ "@vue/shared": "^3.5.0",
+ "alien-signals": "^1.0.3",
+ "minimatch": "^9.0.3",
+ "muggle-string": "^0.4.1",
+ "path-browserify": "^1.0.1"
+ },
+ "peerDependencies": {
+ "typescript": "*"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vue/reactivity": {
+ "version": "3.5.44",
+ "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.44.tgz",
+ "integrity": "sha512-u+QJdKeBBEADkPdevsL+6E12QlYVfBD8Zcw5qjDQKtamI6l14KyqazJMobhCKelhWN5zpyKUiEDoTyHE93cqWw==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/shared": "3.5.44"
+ }
+ },
+ "node_modules/@vue/runtime-core": {
+ "version": "3.5.44",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.44.tgz",
+ "integrity": "sha512-uEa/ndwjhrrhnOfb2p+sZy6zeoamJOISqaLSJGk/8R6nABT1ZldpzcJoPzKj+zHVomVBPcmQYyC2r4LtvDV6CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/reactivity": "3.5.44",
+ "@vue/shared": "3.5.44"
+ }
+ },
+ "node_modules/@vue/runtime-dom": {
+ "version": "3.5.44",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.44.tgz",
+ "integrity": "sha512-3fc4ARcXVhVBaJ3ujWiNZJARD6pkyRcwG+ZYhMzjKtKUp7MAY0HLwr7r3Smep3Li6ZaWT6ewdmx8XqdaD5oNZg==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/reactivity": "3.5.44",
+ "@vue/runtime-core": "3.5.44",
+ "@vue/shared": "3.5.44",
+ "csstype": "^3.2.3"
+ }
+ },
+ "node_modules/@vue/server-renderer": {
+ "version": "3.5.44",
+ "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.44.tgz",
+ "integrity": "sha512-R4S1UoH4h5Frrauv8Z5XE5TxV9oBiUSgWIC4TyVK1FK+RocRnOLwFA5Xcmo2011RlgJp+AbZCnw6afrhlgRuUQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-ssr": "3.5.44",
+ "@vue/runtime-dom": "3.5.44",
+ "@vue/shared": "3.5.44"
+ }
+ },
+ "node_modules/@vue/shared": {
+ "version": "3.5.44",
+ "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.44.tgz",
+ "integrity": "sha512-H82XLH0DjmSOdbEwO15x3QTo1f5PWEtv237eLpYKc0mqLu+B8hnq16lFWmuBmNurkeiG3wr+oM1FhOll/YVFRg==",
+ "license": "MIT"
+ },
+ "node_modules/alien-signals": {
+ "version": "1.0.13",
+ "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-1.0.13.tgz",
+ "integrity": "sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/brace-expansion": {
+ "version": "2.1.7",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz",
+ "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "license": "MIT"
+ },
+ "node_modules/de-indent": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
+ "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/enhanced-resolve": {
+ "version": "5.26.0",
+ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.26.0.tgz",
+ "integrity": "sha512-9vhedylFonb2YGogzUKX6+Ja72gOJbN1QHAqdrvqLwhdl/QWbKopzoUC9EbQNVsAns/bx/4uyqalrQAoy1IByw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.4",
+ "tapable": "^2.3.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/entities": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
+ "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
+ "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.25.12",
+ "@esbuild/android-arm": "0.25.12",
+ "@esbuild/android-arm64": "0.25.12",
+ "@esbuild/android-x64": "0.25.12",
+ "@esbuild/darwin-arm64": "0.25.12",
+ "@esbuild/darwin-x64": "0.25.12",
+ "@esbuild/freebsd-arm64": "0.25.12",
+ "@esbuild/freebsd-x64": "0.25.12",
+ "@esbuild/linux-arm": "0.25.12",
+ "@esbuild/linux-arm64": "0.25.12",
+ "@esbuild/linux-ia32": "0.25.12",
+ "@esbuild/linux-loong64": "0.25.12",
+ "@esbuild/linux-mips64el": "0.25.12",
+ "@esbuild/linux-ppc64": "0.25.12",
+ "@esbuild/linux-riscv64": "0.25.12",
+ "@esbuild/linux-s390x": "0.25.12",
+ "@esbuild/linux-x64": "0.25.12",
+ "@esbuild/netbsd-arm64": "0.25.12",
+ "@esbuild/netbsd-x64": "0.25.12",
+ "@esbuild/openbsd-arm64": "0.25.12",
+ "@esbuild/openbsd-x64": "0.25.12",
+ "@esbuild/openharmony-arm64": "0.25.12",
+ "@esbuild/sunos-x64": "0.25.12",
+ "@esbuild/win32-arm64": "0.25.12",
+ "@esbuild/win32-ia32": "0.25.12",
+ "@esbuild/win32-x64": "0.25.12"
+ }
+ },
+ "node_modules/estree-walker": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
+ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
+ "license": "MIT"
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/he": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
+ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "he": "bin/he"
+ }
+ },
+ "node_modules/jiti": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz",
+ "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jiti": "lib/jiti-cli.mjs"
+ }
+ },
+ "node_modules/lightningcss": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
+ "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
+ "dev": true,
+ "license": "MPL-2.0",
+ "dependencies": {
+ "detect-libc": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "lightningcss-android-arm64": "1.32.0",
+ "lightningcss-darwin-arm64": "1.32.0",
+ "lightningcss-darwin-x64": "1.32.0",
+ "lightningcss-freebsd-x64": "1.32.0",
+ "lightningcss-linux-arm-gnueabihf": "1.32.0",
+ "lightningcss-linux-arm64-gnu": "1.32.0",
+ "lightningcss-linux-arm64-musl": "1.32.0",
+ "lightningcss-linux-x64-gnu": "1.32.0",
+ "lightningcss-linux-x64-musl": "1.32.0",
+ "lightningcss-win32-arm64-msvc": "1.32.0",
+ "lightningcss-win32-x64-msvc": "1.32.0"
+ }
+ },
+ "node_modules/lightningcss-android-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
+ "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
+ "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
+ "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-freebsd-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
+ "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm-gnueabihf": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
+ "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
+ "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
+ "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
+ "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
+ "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-arm64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
+ "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-x64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
+ "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lucide-vue-next": {
+ "version": "0.468.0",
+ "resolved": "https://registry.npmjs.org/lucide-vue-next/-/lucide-vue-next-0.468.0.tgz",
+ "integrity": "sha512-quV/6T8YB1XK0VOEnebg3Byd8Rsan5/m95cvjnuHV4vcS3qEnLAybkrSh0hk3ppavx+V7R1PjNW+mGDvcBdz4A==",
+ "deprecated": "Package deprecated. Please use @lucide/vue instead.",
+ "license": "ISC",
+ "peerDependencies": {
+ "vue": ">=3.0.1"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "9.0.9",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
+ "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/muggle-string": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz",
+ "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.20",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz",
+ "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/path-browserify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
+ "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz",
+ "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.30",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.30.tgz",
+ "integrity": "sha512-zkd+e9S1AQHhmaBKye9zlFHWjOwVgqXl1q3ECYy8561K7qxR2cREf/STLskulBFd78k6AOFeA+ff1TtEzk9bAA==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.20",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.2"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.64.6",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.64.6.tgz",
+ "integrity": "sha512-fX1Vy+UNKdXn+xKDnu/HBXbHVr1AY8cSdQj9BU4kLv5aFCMqGpvrVbyUMmjN3DdzWhSAuTbcX8PCNEVaumthYA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.9"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@napi-rs/lzma-linux-x64-gnu": "1.5.1",
+ "@rollup/rollup-android-arm-eabi": "4.64.6",
+ "@rollup/rollup-android-arm64": "4.64.6",
+ "@rollup/rollup-darwin-arm64": "4.64.6",
+ "@rollup/rollup-darwin-x64": "4.64.6",
+ "@rollup/rollup-freebsd-arm64": "4.64.6",
+ "@rollup/rollup-freebsd-x64": "4.64.6",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.64.6",
+ "@rollup/rollup-linux-arm-musleabihf": "4.64.6",
+ "@rollup/rollup-linux-arm64-gnu": "4.64.6",
+ "@rollup/rollup-linux-arm64-musl": "4.64.6",
+ "@rollup/rollup-linux-loong64-gnu": "4.64.6",
+ "@rollup/rollup-linux-loong64-musl": "4.64.6",
+ "@rollup/rollup-linux-ppc64-gnu": "4.64.6",
+ "@rollup/rollup-linux-ppc64-musl": "4.64.6",
+ "@rollup/rollup-linux-riscv64-gnu": "4.64.6",
+ "@rollup/rollup-linux-riscv64-musl": "4.64.6",
+ "@rollup/rollup-linux-s390x-gnu": "4.64.6",
+ "@rollup/rollup-linux-x64-gnu": "4.64.6",
+ "@rollup/rollup-linux-x64-musl": "4.64.6",
+ "@rollup/rollup-openbsd-x64": "4.64.6",
+ "@rollup/rollup-openharmony-arm64": "4.64.6",
+ "@rollup/rollup-win32-arm64-msvc": "4.64.6",
+ "@rollup/rollup-win32-ia32-msvc": "4.64.6",
+ "@rollup/rollup-win32-x64-gnu": "4.64.6",
+ "@rollup/rollup-win32-x64-msvc": "4.64.6",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz",
+ "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/tailwindcss": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz",
+ "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tapable": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz",
+ "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.17",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
+ "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "devOptional": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/vite": {
+ "version": "6.4.4",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.4.tgz",
+ "integrity": "sha512-xGjU/SyjeNyb0E4KxXBvYz/1C+3JXuujvf2hXNOtnlcE+58o5ajd9N2/n2VhkiJvpCP2qQasnUpx5PXNeI+p1Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.25.0",
+ "fdir": "^6.4.4",
+ "picomatch": "^4.0.2",
+ "postcss": "^8.5.3",
+ "rollup": "^4.34.9",
+ "tinyglobby": "^0.2.13"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
+ "jiti": ">=1.21.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vscode-uri": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.2.0.tgz",
+ "integrity": "sha512-m2gXo3bn0G1kT9InzMf07fTbqMbGtyckj3bH5ktLO+1Ssv+yiATZ4dhwaQv9UZWxJh6E9IFGnQyjgWVDWVBDrg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/vue": {
+ "version": "3.5.44",
+ "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.44.tgz",
+ "integrity": "sha512-dvvmaOCG7lAMkJTPqPwWeGMsPS6DIsFsfZDUgBp9V6mHf9b3+/wyVwYR4Es+WV6uJC0FKRxp81Obcq0ZwUcGFQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-dom": "3.5.44",
+ "@vue/compiler-sfc": "3.5.44",
+ "@vue/runtime-dom": "3.5.44",
+ "@vue/server-renderer": "3.5.44",
+ "@vue/shared": "3.5.44"
+ },
+ "peerDependencies": {
+ "typescript": "*"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vue-tsc": {
+ "version": "2.2.12",
+ "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.2.12.tgz",
+ "integrity": "sha512-P7OP77b2h/Pmk+lZdJ0YWs+5tJ6J2+uOQPo7tlBnY44QqQSPYvS0qVT4wqDJgwrZaLe47etJLLQRFia71GYITw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@volar/typescript": "2.4.15",
+ "@vue/language-core": "2.2.12"
+ },
+ "bin": {
+ "vue-tsc": "bin/vue-tsc.js"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.0.0"
+ }
+ }
+ }
+}
diff --git a/projects/moritz/package.json b/projects/moritz/package.json
new file mode 100644
index 0000000..471231d
--- /dev/null
+++ b/projects/moritz/package.json
@@ -0,0 +1,25 @@
+{
+ "name": "moritz-tracking",
+ "private": true,
+ "version": "1.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vue-tsc -b && vite build",
+ "preview": "vite preview",
+ "typecheck": "vue-tsc --noEmit"
+ },
+ "dependencies": {
+ "lucide-vue-next": "^0.468.0",
+ "vue": "^3.5.13"
+ },
+ "devDependencies": {
+ "@tailwindcss/vite": "^4.0.9",
+ "@types/node": "^22.10.2",
+ "@vitejs/plugin-vue": "^5.2.1",
+ "tailwindcss": "^4.0.9",
+ "typescript": "^5.7.2",
+ "vite": "^6.2.0",
+ "vue-tsc": "^2.2.0"
+ }
+}
diff --git a/projects/moritz/public/favicon.ico b/projects/moritz/public/favicon.ico
new file mode 100644
index 0000000..2a71bec
Binary files /dev/null and b/projects/moritz/public/favicon.ico differ
diff --git a/projects/moritz/public/manifest.json b/projects/moritz/public/manifest.json
new file mode 100644
index 0000000..9c174ae
--- /dev/null
+++ b/projects/moritz/public/manifest.json
@@ -0,0 +1,48 @@
+{
+ "id": "/tracking/moritz/",
+ "name": "Moritz Health — Energiebilanz & Sport",
+ "short_name": "Health",
+ "description": "Persönliches Dashboard für Kalorienbilanz, Ernährung und Garmin-Fitness von Moritz.",
+ "start_url": "/tracking/moritz/",
+ "scope": "/tracking/moritz/",
+ "display": "standalone",
+ "orientation": "portrait-primary",
+ "background_color": "#f8fafc",
+ "theme_color": "#10b981",
+ "icons": [
+ {
+ "src": "/tracking/moritz/moritz-icon-192.png",
+ "sizes": "192x192",
+ "type": "image/png",
+ "purpose": "any"
+ },
+ {
+ "src": "/tracking/moritz/moritz-icon-192.png",
+ "sizes": "192x192",
+ "type": "image/png",
+ "purpose": "maskable"
+ },
+ {
+ "src": "/tracking/moritz/moritz-icon-512.png",
+ "sizes": "512x512",
+ "type": "image/png",
+ "purpose": "any"
+ },
+ {
+ "src": "/tracking/moritz/moritz-icon-512.png",
+ "sizes": "512x512",
+ "type": "image/png",
+ "purpose": "maskable"
+ },
+ {
+ "src": "/tracking/moritz/moritz-apple-touch-icon.png",
+ "sizes": "180x180",
+ "type": "image/png"
+ },
+ {
+ "src": "/tracking/moritz/moritz-icon.svg",
+ "sizes": "any",
+ "type": "image/svg+xml"
+ }
+ ]
+}
diff --git a/projects/moritz/public/moritz-apple-touch-icon.png b/projects/moritz/public/moritz-apple-touch-icon.png
new file mode 100644
index 0000000..ca3d841
Binary files /dev/null and b/projects/moritz/public/moritz-apple-touch-icon.png differ
diff --git a/projects/moritz/public/moritz-icon-16.png b/projects/moritz/public/moritz-icon-16.png
new file mode 100644
index 0000000..fdcf66e
Binary files /dev/null and b/projects/moritz/public/moritz-icon-16.png differ
diff --git a/projects/moritz/public/moritz-icon-192.png b/projects/moritz/public/moritz-icon-192.png
new file mode 100644
index 0000000..4f286cc
Binary files /dev/null and b/projects/moritz/public/moritz-icon-192.png differ
diff --git a/projects/moritz/public/moritz-icon-32.png b/projects/moritz/public/moritz-icon-32.png
new file mode 100644
index 0000000..6d7441e
Binary files /dev/null and b/projects/moritz/public/moritz-icon-32.png differ
diff --git a/projects/moritz/public/moritz-icon-512.png b/projects/moritz/public/moritz-icon-512.png
new file mode 100644
index 0000000..784eeeb
Binary files /dev/null and b/projects/moritz/public/moritz-icon-512.png differ
diff --git a/projects/moritz/public/moritz-icon.svg b/projects/moritz/public/moritz-icon.svg
new file mode 100644
index 0000000..31df281
--- /dev/null
+++ b/projects/moritz/public/moritz-icon.svg
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/projects/moritz/public/moritz.webmanifest b/projects/moritz/public/moritz.webmanifest
new file mode 100644
index 0000000..9c174ae
--- /dev/null
+++ b/projects/moritz/public/moritz.webmanifest
@@ -0,0 +1,48 @@
+{
+ "id": "/tracking/moritz/",
+ "name": "Moritz Health — Energiebilanz & Sport",
+ "short_name": "Health",
+ "description": "Persönliches Dashboard für Kalorienbilanz, Ernährung und Garmin-Fitness von Moritz.",
+ "start_url": "/tracking/moritz/",
+ "scope": "/tracking/moritz/",
+ "display": "standalone",
+ "orientation": "portrait-primary",
+ "background_color": "#f8fafc",
+ "theme_color": "#10b981",
+ "icons": [
+ {
+ "src": "/tracking/moritz/moritz-icon-192.png",
+ "sizes": "192x192",
+ "type": "image/png",
+ "purpose": "any"
+ },
+ {
+ "src": "/tracking/moritz/moritz-icon-192.png",
+ "sizes": "192x192",
+ "type": "image/png",
+ "purpose": "maskable"
+ },
+ {
+ "src": "/tracking/moritz/moritz-icon-512.png",
+ "sizes": "512x512",
+ "type": "image/png",
+ "purpose": "any"
+ },
+ {
+ "src": "/tracking/moritz/moritz-icon-512.png",
+ "sizes": "512x512",
+ "type": "image/png",
+ "purpose": "maskable"
+ },
+ {
+ "src": "/tracking/moritz/moritz-apple-touch-icon.png",
+ "sizes": "180x180",
+ "type": "image/png"
+ },
+ {
+ "src": "/tracking/moritz/moritz-icon.svg",
+ "sizes": "any",
+ "type": "image/svg+xml"
+ }
+ ]
+}
diff --git a/projects/moritz/public/sw.js b/projects/moritz/public/sw.js
new file mode 100644
index 0000000..0756d86
--- /dev/null
+++ b/projects/moritz/public/sw.js
@@ -0,0 +1,51 @@
+// Minimal service worker for Moritz Health PWA with instant cache-bypassing navigation
+const CACHE_NAME = 'moritz-pwa-v5';
+
+self.addEventListener('install', (event) => {
+ self.skipWaiting();
+});
+
+self.addEventListener('activate', (event) => {
+ event.waitUntil(
+ caches.keys().then((keys) =>
+ Promise.all(
+ keys.map((key) => {
+ if (key !== CACHE_NAME) return caches.delete(key);
+ })
+ )
+ ).then(() => self.clients.claim())
+ );
+});
+
+self.addEventListener('fetch', (event) => {
+ if (event.request.method !== 'GET') return;
+
+ // For HTML documents/navigations: ALWAYS bypass HTTP cache to get latest live server rendering
+ if (event.request.destination === 'document' || event.request.mode === 'navigate') {
+ event.respondWith(
+ fetch(event.request, { cache: 'no-cache' })
+ .then((response) => {
+ if (response && response.status === 200) {
+ const clone = response.clone();
+ caches.open(CACHE_NAME).then((cache) => cache.put(event.request, clone));
+ }
+ return response;
+ })
+ .catch(() => caches.match(event.request))
+ );
+ return;
+ }
+
+ // Assets (images, fonts, scripts): network-first with offline cache fallback
+ event.respondWith(
+ fetch(event.request)
+ .then((response) => {
+ if (response && response.status === 200) {
+ const clone = response.clone();
+ caches.open(CACHE_NAME).then((cache) => cache.put(event.request, clone));
+ }
+ return response;
+ })
+ .catch(() => caches.match(event.request))
+ );
+});
diff --git a/projects/moritz/src/App.vue b/projects/moritz/src/App.vue
new file mode 100644
index 0000000..8d8e9e2
--- /dev/null
+++ b/projects/moritz/src/App.vue
@@ -0,0 +1,763 @@
+
+
+
+
+
+
+
+
+
+
+ Git-Synchronisationskonflikt: Lokale Änderungen werden zwischengespeichert bis zur Auflösung.
+
+
+
+
+
+
+
+ {{ fetchError }}
+ Erneut versuchen
+
+
+
+
+
+
+
+
+
+
+
+
{{ data?.metadata?.today_pretty || 'Lade...' }}
+
+
+
+
+
+
+
+
+ Installieren
+
+
+
+
+
+
+
+
+
+
+ Heute ({{ data.today.item_count }})
+
+
+
+
+
+ Erfassen
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Heute (Intake bisher)
+
+
+ Mahlzeiten
+
+
+
+
+
+
+ kcal
+
+
+ {{ data.today.item_count }} Mahlzeiten erfasst · Tippen für Details
+
+
+
+
+
+
+
+
{{ data.today.protein_g }}g
+
Protein
+
+
+
{{ data.today.carbs_g }}g
+
Carbs
+
+
+
{{ data.today.fat_g }}g
+
Fett
+
+
+
+
+
+
+
+
+
+
+ Gestern (24h Netto-Bilanz)
+
+
+ Deep Dive
+
+
+
+
+
+
+
+ {{ data.yesterday.balance_status }}
+
+
+
+
+
+
+
+ {{ data.yesterday.intake_kcal.toLocaleString('de-DE') }}
+
+
−
+
+
+ {{ data.yesterday.expenditure_kcal.toLocaleString('de-DE') }}
+
+
=
+
+ {{ data.yesterday.balance_kcal !== null ? `${data.yesterday.balance_kcal > 0 ? '+' : ''}${data.yesterday.balance_kcal}` : '—' }} kcal
+
+
+
+
+
+
+
+
+ {{ data.yesterday.protein_g }}g P
+ ·
+ {{ data.yesterday.carbs_g }}g C
+ ·
+ {{ data.yesterday.fat_g }}g F
+
+
+
+ {{ data.yesterday.steps.toLocaleString('de-DE') }} Schritte
+
+
+
+
+
+
+
+
+
+
+
⌚
+
+
Garmin Schlaf & Erholung
+
{{ data.metadata.sleep_date_label }}
+
+
+
+ Score {{ data.garmin_recovery.sleep_score }} ({{ data.garmin_recovery.sleep_qualifier }})
+
+
+
+
+
+
Schlafdauer
+
{{ data.garmin_recovery.sleep_dur_str }}
+
Gesamtschlaf
+
+
+
HRV Status
+
+ {{ data.garmin_recovery.hrv_val ? `${data.garmin_recovery.hrv_val} ms` : '—' }}
+
+
{{ data.garmin_recovery.hrv_status || 'Ausgeglichen' }}
+
+
+
Ruhepuls
+
+ {{ data.garmin_recovery.resting_hr ? `${data.garmin_recovery.resting_hr} bpm` : '—' }}
+
+
Nacht-Minimum
+
+
+
Body Battery
+
+ {{ data.garmin_recovery.bb_change !== null ? `+${data.garmin_recovery.bb_change}` : '—' }}
+
+
Übernacht-Aufladung
+
+
+
+
+
+
+
+
+
+
+
+
📅
+
+
Metabolische Historie
+
Netto-Energiebilanzen & Makro-Historie
+
+
+
+
+
+
+
+ {{ p === '7d' ? '7 Tage' : p === '14d' ? '14 Tage' : '30 Tage' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ row.weekday }}, {{ row.date.slice(5) }}
+
+
+ {{ row.balance_status }}
+
+
+
+ Intake: {{ row.intake_kcal }} kcal
+ ·
+ Verbrauch: {{ row.expenditure_kcal }} kcal
+
+
+
+
+
+
+
+
+ {{ row.steps ? `${row.steps.toLocaleString('de-DE')} Schritte` : '' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
Protein
+
{{ row.protein_g }}g
+
+
+
Kohlenhydrate
+
{{ row.carbs_g }}g
+
+
+
Fett
+
{{ row.fat_g }}g
+
+
+
Ruhepuls
+
{{ row.resting_hr ? `${row.resting_hr} bpm` : '—' }}
+
+
+
+
+
+
+
+
+
+
+
+ Datum
+ Intake
+ Verbrauch
+ Netto-Bilanz
+ Status
+ Schritte
+ Detail
+
+
+
+
+
+ {{ row.weekday }}, {{ row.date.slice(5) }}
+
+
+ {{ row.intake_kcal ? `${row.intake_kcal.toLocaleString('de-DE')} kcal` : '—' }}
+
+
+ {{ row.expenditure_kcal ? `${row.expenditure_kcal.toLocaleString('de-DE')} kcal` : '—' }}
+
+
+ {{ row.balance_kcal !== null ? `${row.balance_kcal > 0 ? '+' : ''}${row.balance_kcal} kcal` : '—' }}
+
+
+
+ {{ row.balance_status }}
+
+
+
+ {{ row.steps ? row.steps.toLocaleString('de-DE') : '—' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Lade Moritz Dashboard aus DuckDB...
+
+
+
+
+
+ Erfassen
+
+
+
+
+
+
+
+
+
+ {{ activeDetailModal === 'today_meals' ? 'Heutige Mahlzeiten' : 'Gestrige Mahlzeiten' }}
+
+
+
+
+
+
+
+
+
+
+
+
{{ meal.item }}
+
+ {{ meal.time }} Uhr · {{ meal.qty ? `${meal.qty} ${meal.unit}` : '' }}
+
+
+ {{ meal.protein_g }}g P
+ ·
+ {{ meal.carbs_g }}g C
+ ·
+ {{ meal.fat_g }}g F
+
+
+
+ {{ meal.kcal }} kcal
+
+
+
+
+
+
+ Keine Mahlzeiten für diesen Tag verzeichnet.
+
+
+
+
+
+
+
+
+
+
+
+
+ Mahlzeiten: {{ selectedDayDetail ? `${selectedDayDetail.weekday}, ${selectedDayDetail.date}` : 'Lade...' }}
+
+
Mahlzeiten und Nährwerte dieses Tages
+
+
+
+
+
+
+
+
+ Lade Mahlzeiten...
+
+
+
+
+
+
+ Tages-Intake:
+ {{ selectedDayDetail.nutrition.total_kcal }} kcal
+
+
+ {{ selectedDayDetail.nutrition.protein_g }}g P
+ ·
+ {{ selectedDayDetail.nutrition.carbs_g }}g C
+ ·
+ {{ selectedDayDetail.nutrition.fat_g }}g F
+
+
+
+
+
+
+
+
{{ meal.item }}
+
+ {{ meal.time }} Uhr · {{ meal.qty ? `${meal.qty} ${meal.unit}` : '' }}
+
+
+ {{ meal.protein_g }}g P
+ ·
+ {{ meal.carbs_g }}g C
+ ·
+ {{ meal.fat_g }}g F
+
+
+
+ {{ meal.kcal }} kcal
+
+
+
+
+
+
+ Keine Mahlzeiten erfasst.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Moritz Health auf Home-Bildschirm
+
+
✕
+
+
+ So installierst du die Health-App auf deinem iPhone oder iPad:
+
+
+ Tippe in Safari unten auf das Teilen-Symbol (Quadrat mit Pfeil nach oben).
+ Scrolle im Menü nach unten und wähle „Zum Home-Bildschirm“ (+).
+ Tippe oben rechts auf „Hinzufügen“ .
+
+
+ Verstanden
+
+
+
+
+
+
diff --git a/projects/moritz/src/api.ts b/projects/moritz/src/api.ts
new file mode 100644
index 0000000..35c440a
--- /dev/null
+++ b/projects/moritz/src/api.ts
@@ -0,0 +1,96 @@
+import type { MoritzDashboardData, MoritzDayDetail, FoodEntity } from './types'
+
+const API_BASE = '/duckdb-api/v1'
+
+export async function fetchMoritzDashboard(asOf?: string): Promise {
+ const url = asOf ? `${API_BASE}/moritz/dashboard?as_of=${encodeURIComponent(asOf)}` : `${API_BASE}/moritz/dashboard`
+ const res = await fetch(url)
+ if (!res.ok) {
+ throw new Error(`Failed to fetch dashboard: ${res.status} ${res.statusText}`)
+ }
+ return res.json()
+}
+
+export async function fetchMoritzDayDetail(date: string): Promise {
+ const res = await fetch(`${API_BASE}/moritz/day/${encodeURIComponent(date)}`)
+ if (!res.ok) {
+ throw new Error(`Failed to fetch day detail for ${date}: ${res.statusText}`)
+ }
+ return res.json()
+}
+
+export async function fetchFoods(): Promise {
+ try {
+ const res = await fetch(`${API_BASE}/entities/foods`)
+ if (!res.ok) return []
+ const json = await res.json()
+ return json.items || []
+ } catch {
+ return []
+ }
+}
+
+export function getLocalDateString(): string {
+ const d = new Date()
+ const year = d.getFullYear()
+ const month = String(d.getMonth() + 1).padStart(2, '0')
+ const day = String(d.getDate()).padStart(2, '0')
+ return `${year}-${month}-${day}`
+}
+
+export async function logNutritionIntake(payload: {
+ item: string
+ qty?: number
+ unit?: string
+ kcal?: number
+ protein_g?: number
+ carbs_g?: number
+ fat_g?: number
+ meal?: string
+ time?: string
+ date?: string
+ note?: string
+}): Promise {
+ const baseDate = payload.date || getLocalDateString()
+ const startIso = payload.time ? `${baseDate}T${payload.time}:00` : undefined
+
+ const res = await fetch(`${API_BASE}/events`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ kind: 'nutrition.intake',
+ subject: 'moritz',
+ payload: {
+ item: payload.item,
+ qty: payload.qty,
+ unit: payload.unit || 'g',
+ kcal: payload.kcal,
+ protein_g: payload.protein_g,
+ carbs_g: payload.carbs_g,
+ fat_g: payload.fat_g,
+ },
+ meal: payload.meal || 'snack',
+ date: baseDate,
+ start: startIso,
+ note: payload.note,
+ }),
+ })
+
+ if (!res.ok) {
+ const err = await res.json().catch(() => ({ detail: res.statusText }))
+ throw new Error(err.detail || 'Failed to log nutrition intake')
+ }
+ return res.json()
+}
+
+export async function deleteNutritionEvent(eventId: string): Promise {
+ const res = await fetch(`${API_BASE}/events/${encodeURIComponent(eventId)}`, {
+ method: 'DELETE',
+ })
+ if (!res.ok) {
+ const err = await res.json().catch(() => ({ detail: res.statusText }))
+ throw new Error(err.detail || 'Failed to delete event')
+ }
+ return res.json()
+}
+
diff --git a/projects/moritz/src/components/EnergyBalanceChart.vue b/projects/moritz/src/components/EnergyBalanceChart.vue
new file mode 100644
index 0000000..34fe0c1
--- /dev/null
+++ b/projects/moritz/src/components/EnergyBalanceChart.vue
@@ -0,0 +1,563 @@
+
+
+
+
+
+
+
+
+ ⚡
+
+
+
Metabolische Energiebilanz & Trends
+
Täglicher Vergleich Intake vs. Garmin-TDEE
+
+
+
+
+
+
+
+ Bilanz
+
+
+ Makros
+
+
+ Garmin
+
+
+
+
+
+
+ {{ p === '7d' ? '7 Tage' : p === '14d' ? '14 Tage' : '30 Tage' }}
+
+
+
+
+
+
+
+
+
+ {{ activeItem.day.weekday }}, {{ activeItem.day.date.slice(5) }}
+
+
+ {{ activeItem.day.balance_status }}
+
+
+
+
+
+
+
+ {{ activeItem.day.intake_kcal }} kcal
+
+
+
+ {{ activeItem.day.expenditure_kcal }} kcal
+
+
+ Netto:
+
+ {{ activeItem.day.balance_kcal !== null ? `${activeItem.day.balance_kcal > 0 ? '+' : ''}${activeItem.day.balance_kcal}` : '—' }} kcal
+
+
+
+ {{ activeItem.day.steps ? `${activeItem.day.steps.toLocaleString('de-DE')} Schritte` : '' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ maxCalorieScale }}
+ {{ maxCalorieScale * 0.5 }}
+ 0
+
+
+ {{ maxMacroScale }}g
+ {{ maxMacroScale * 0.5 }}g
+ 0g
+
+
+ 10k
+ 5k
+ 0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.day.weekday }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.day.weekday }}
+
+
+
+
+
+
+
+
+
+
+ {{ item.day.weekday }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Intake (kcal)
+
+
+
+ Garmin TDEE (kcal)
+
+
+
+
+
+ Protein
+
+
+
+ Carbs
+
+
+
+ Fett
+
+
+
+
+
+ Schritte
+
+
+
+ Ruhepuls (bpm)
+
+
+
+
+
+
+ Kumulierte Bilanz:
+
+ {{ summary.totalNet > 0 ? '+' : '' }}{{ summary.totalNet.toLocaleString('de-DE') }} kcal
+
+
+
+ ≈ {{ summary.fatKg > 0 ? `+${summary.fatKg}` : summary.fatKg }} kg Fettgewebe
+
+
+
+
+
diff --git a/projects/moritz/src/components/MealTrackingModal.vue b/projects/moritz/src/components/MealTrackingModal.vue
new file mode 100644
index 0000000..8f4d39d
--- /dev/null
+++ b/projects/moritz/src/components/MealTrackingModal.vue
@@ -0,0 +1,429 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Mahlzeit erfassen
+
Ernährungs-Tracking in Knowledge Wiki
+
+
+
+
+
+
+
+
+
+
+ 📅
+ Datum des Eintrags:
+
+
+
+
+
+
+ {{ errorMessage }}
+
+
+
+ {{ successMessage }}
+
+
+
+
+
+
+
+
+
+
+ 1-Tap Favoriten & Snacks
+
+
+
+
+
+
{{ q.title }}
+
{{ q.desc }}
+
+ {{ q.kcal }} kcal · {{ q.protein_g }}g P · {{ q.carbs_g }}g C
+
+
+
+ + Jetzt
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/projects/moritz/src/main.ts b/projects/moritz/src/main.ts
new file mode 100644
index 0000000..d7b7e26
--- /dev/null
+++ b/projects/moritz/src/main.ts
@@ -0,0 +1,25 @@
+///
+import { createApp } from 'vue'
+import './style.css'
+import App from './App.vue'
+
+createApp(App).mount('#app')
+
+// Reliable Service Worker registration (catches readyState complete and load)
+if ('serviceWorker' in navigator) {
+ const registerSW = () => {
+ navigator.serviceWorker
+ .register('/tracking/moritz/sw.js', { scope: '/tracking/moritz/' })
+ .then((reg) => {
+ // Check for updates
+ reg.update().catch(() => {})
+ })
+ .catch((err) => console.warn('Moritz Service Worker registration failed:', err))
+ }
+
+ if (document.readyState === 'complete') {
+ registerSW()
+ } else {
+ window.addEventListener('load', registerSW)
+ }
+}
diff --git a/projects/moritz/src/style.css b/projects/moritz/src/style.css
new file mode 100644
index 0000000..bf370bc
--- /dev/null
+++ b/projects/moritz/src/style.css
@@ -0,0 +1,12 @@
+@import "tailwindcss";
+
+@layer base {
+ body {
+ font-family: 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
+ -webkit-tap-highlight-color: transparent;
+ }
+
+ .font-mono {
+ font-family: 'JetBrains Mono', monospace;
+ }
+}
diff --git a/projects/moritz/src/types.ts b/projects/moritz/src/types.ts
new file mode 100644
index 0000000..57a9843
--- /dev/null
+++ b/projects/moritz/src/types.ts
@@ -0,0 +1,149 @@
+export interface MealItem {
+ id?: string
+ time: string
+ item: string
+ qty: number | null
+ unit: string
+ protein_g: number
+ carbs_g: number
+ fat_g: number
+ fiber_g: number
+ kcal: number
+ estimated?: boolean
+}
+
+export interface FoodEntity {
+ id: string
+ title: string
+ brand?: string
+ default_portion_g?: number
+ default_unit?: string
+ per_100g?: {
+ kcal?: number
+ protein?: number
+ carbs?: number
+ fat?: number
+ fiber?: number
+ }
+}
+
+export interface MoritzDayDetail {
+ date: string
+ weekday: string
+ nutrition: {
+ total_kcal: number
+ protein_g: number
+ carbs_g: number
+ fat_g: number
+ meals: MealItem[]
+ }
+ garmin?: {
+ total_kcal?: number
+ active_kcal?: number
+ bmr_kcal?: number
+ steps?: number
+ resting_hr?: number
+ }
+ balance_kcal?: number | null
+}
+
+export interface DayBar {
+ date: string
+ weekday: string
+ intake_kcal: number
+ expenditure_kcal: number
+ active_kcal: number
+ steps: number
+ balance_kcal: number | null
+ protein_g: number
+ carbs_g: number
+ fat_g: number
+ resting_hr: number | null
+}
+
+export interface HistoryDay {
+ date: string
+ weekday: string
+ intake_kcal: number
+ expenditure_kcal: number
+ active_kcal: number
+ rmr_kcal: number
+ steps: number
+ balance_kcal: number | null
+ balance_status: string
+ protein_g: number
+ carbs_g: number
+ fat_g: number
+ resting_hr: number | null
+}
+
+export interface MoritzDashboardData {
+ status: string
+ execution_time_ms: number
+ git_status: string
+ git_conflict: boolean
+ metadata: {
+ subject: string
+ today_date: string
+ today_pretty: string
+ yesterday_date: string
+ yesterday_pretty: string
+ sleep_date_label: string
+ }
+ today: {
+ total_kcal: number
+ protein_g: number
+ carbs_g: number
+ fat_g: number
+ item_count: number
+ meals: MealItem[]
+ }
+ yesterday: {
+ intake_kcal: number
+ expenditure_kcal: number
+ active_kcal: number
+ rmr_kcal: number
+ steps: number
+ balance_kcal: number | null
+ balance_status: string
+ protein_g: number
+ carbs_g: number
+ fat_g: number
+ resting_hr: number | null
+ meals: MealItem[]
+ }
+ garmin_recovery: {
+ sleep_dur_str: string
+ sleep_score: number | null
+ sleep_qualifier: string
+ hrv_val: number | null
+ hrv_status: string
+ bb_change: number | null
+ resting_hr: number | null
+ deep_minutes: number
+ light_minutes: number
+ rem_minutes: number
+ awake_minutes: number
+ deep_pct: number
+ light_pct: number
+ rem_pct: number
+ awake_pct: number
+ }
+ seven_days: {
+ days: DayBar[]
+ avg_intake_kcal: number
+ avg_expenditure_kcal: number
+ avg_balance_kcal: number
+ avg_steps: number
+ avg_protein_g: number
+ }
+ thirty_days: {
+ days: HistoryDay[]
+ avg_intake_kcal: number
+ avg_expenditure_kcal: number
+ avg_balance_kcal: number
+ total_balance_kcal: number
+ avg_steps: number
+ }
+}
+
diff --git a/projects/moritz/tsconfig.json b/projects/moritz/tsconfig.json
new file mode 100644
index 0000000..395c931
--- /dev/null
+++ b/projects/moritz/tsconfig.json
@@ -0,0 +1,20 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "module": "ESNext",
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "skipLibCheck": true,
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "isolatedModules": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+ "jsx": "preserve",
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
+}
diff --git a/projects/moritz/vite.config.ts b/projects/moritz/vite.config.ts
new file mode 100644
index 0000000..3882be2
--- /dev/null
+++ b/projects/moritz/vite.config.ts
@@ -0,0 +1,20 @@
+import { defineConfig } from 'vite'
+import vue from '@vitejs/plugin-vue'
+import tailwindcss from '@tailwindcss/vite'
+
+export default defineConfig({
+ base: '/tracking/moritz/',
+ plugins: [
+ vue(),
+ tailwindcss(),
+ ],
+ server: {
+ port: 5174,
+ proxy: {
+ '/duckdb-api': {
+ target: 'http://localhost:8000',
+ changeOrigin: true,
+ },
+ },
+ },
+})