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 +
+
+
+

+ Mila Graf +

+ + 6 Monate, 4 Wochen alt + +
+

Dienstag, 13.10.2026

+
+
+ + +
+
Wach (11 h 11 min)
+ + +
+
+
+ +
+ + +
+ + +
+
+
+ ☀️ Status: Wach & Munter +
+
+ Wachfenster: 11 h 11 min +
+

+ Letztes Schläfchen beendet um 18:15 Uhr. +

+
+
+ + +
+
+
24h Schlaf Heute
+
0 h
+
Nacht + Schläfchen
+
+
+
Tagesschläfchen
+
0 Naps
+
0 min gesamt
+
+
+
Nachtschlaf
+
0 min
+
Hauptschlafphase
+
+
+
7-Tage-Schnitt
+
12.12 h
+
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 +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Datum24h GesamtNachtSchläfchenFläschchenWindeln
+ Mo, 12.10. + + 2.67 h0 min4 (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 h10 h 45 min3 (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 h11 h 40 min2 (161 min)——
+ Einzelschläfchen: 12:22–14:25 (123m) 16:10–16:48 (38m) 19:20–07:00 (700m) +
+ Fr, 09.10. + + 12.42 h11 h 20 min3 (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 h11 h 45 min3 (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 h12 h 20 min3 (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 h11 h 40 min4 (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 h55 min4 (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 h0 min4 (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 h10 h 05 min5 (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 h9 h 30 min4 (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 h9 h 30 min3 (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 h11 h 17 min3 (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 h11 h 53 min4 (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 h23 h 34 min4 (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 h10 h 10 min4 (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 h23 h 50 min3 (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 h11 h 33 min5 (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 h23 h 02 min4 (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 h11 h 18 min4 (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 h23 h 12 min5 (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 h0 min5 (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 h10 h 55 min5 (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 h11 h 55 min4 (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) +
+
+
+ + + +
+ + + + +
+ +
+ + Noch keine Messungen erfasst + +
+ + +
+ +
+ + +
+ + +
+ + Mila + + + WHO Median (P50) + + + P3–P97 Normkorridor (94%) + +
+
+ + +
+ +
+ + +
+
+

+ 📋 + Erfasste Messungen +

+ 0 Messung(en) +
+ +
+ + + + + + + + + + + + + + + + + +
DatumAlterGewichtKörpergrößeNotiz / Anlass
+ Bisher noch keine manuellen Messwerte eingetragen. Messungen können per Telegram oder CLI erfasst werden. +
+
+
+ +
+
+ + +
+ + + + + + + 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 @@ + + + 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 @@ + + + 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 @@ + + + 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 @@ + + + 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 @@ + + + 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 @@ + + + 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 @@ + + + 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 @@ + + + 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, + }, + }, + }, +})