Content
<p align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://img.shields.io/badge/Vitals-Personal_Health_Dashboard-F5A623?style=for-the-badge&labelColor=1a1a2e">
<img alt="Vitals" src="https://img.shields.io/badge/Vitals-Personal_Health_Dashboard-F5A623?style=for-the-badge&labelColor=1a1a2e">
</picture>
</p>
<p align="center">
<strong>Self-hosted health data lake with 25 MCP tools for Claude.ai</strong><br>
<sub>OAuth 2.0 · 12 health domains · Weight · GLP-1 · Garmin · Hevy · Nutrition · Labs · Genetics · Skincare · AI Digests</sub>
</p>
<p align="center">
<img src="https://img.shields.io/github/stars/ilodezis/vitals?style=flat-square" alt="Stars">
<img src="https://img.shields.io/github/last-commit/ilodezis/vitals?style=flat-square" alt="Last Commit">
<img src="https://img.shields.io/badge/Python-3.11-3776AB?style=flat-square&logo=python&logoColor=white" alt="Python">
<img src="https://img.shields.io/badge/FastAPI-0.115+-009688?style=flat-square&logo=fastapi&logoColor=white" alt="FastAPI">
<img src="https://img.shields.io/badge/PostgreSQL-15-4169E1?style=flat-square&logo=postgresql&logoColor=white" alt="PostgreSQL">
<img src="https://img.shields.io/badge/Redis-7-DC382D?style=flat-square&logo=redis&logoColor=white" alt="Redis">
<img src="https://img.shields.io/badge/Docker-Compose-2496ED?style=flat-square&logo=docker&logoColor=white" alt="Docker">
<img src="https://img.shields.io/badge/HTMX-1.9-3D72D7?style=flat-square" alt="HTMX">
<img src="https://img.shields.io/badge/Alpine.js-3.x-8BC0D0?style=flat-square&logo=alpinedotjs&logoColor=white" alt="Alpine.js">
<img src="https://img.shields.io/badge/Tailwind-3.0-38BDF8?style=flat-square&logo=tailwindcss&logoColor=white" alt="Tailwind">
<img src="https://img.shields.io/badge/MCP-Claude_AI-D4A574?style=flat-square" alt="MCP">
<img src="https://img.shields.io/badge/PWA-Mobile_First-5A0FC8?style=flat-square&logo=pwa&logoColor=white" alt="PWA">
<img src="https://img.shields.io/badge/License-PolyForm_NC-green?style=flat-square" alt="License">
</p>
<p align="center">
<img src="./gifs/MCP_en.gif" alt="Claude querying Vitals via MCP" width="100%">
</p>
<p align="center">
<a href="#vitals---personal-health-dashboard"><img src="https://raw.githubusercontent.com/lipis/flag-icons/main/flags/4x3/us.svg" width="20" valign="middle" alt="en"> English</a>
</p>
---
## Vitals - Personal Health Dashboard
**Vitals** — a personal health dashboard and a full-fledged data lake for one user. The system is designed for long-term tracking of biomarkers, body recomposition, GLP-1 therapy control, sports indicator analysis, and weekly AI reports using language models via OpenRouter.
The main difference from fitness trackers is the **principle of maximum preservation of raw data**. Vitals is a smart health navigator that highlights non-obvious relationships between sleep, workouts, medications, and weight, helping you make informed decisions. Not a supervisor - a navigator.
### Why I created this
My data was scattered across six apps: Garmin, Hevy, GLP-1 notes, a table with analyzes, and a file with supplements. None of them could correlate sleep with recovery after a workout or link side effects of therapy with nutrition - everything had to be kept in mind.
Vitals was written with Claude as the primary development tool - but the data model, architecture, and domain structure are mine. The MCP layer is a key part: Claude can read and write data in real-time across all 12 domains, answering questions like "how does my sleep over the past week affect recovery?" with specific numbers.
### What it can do
<p align="center">
<img src="./gifs/dashboard_en.gif" alt="Vitals dashboard walkthrough" width="100%">
</p>
<table>
<tr>
<td width="50%">
**📊 Data and Analytics**
- 12 independent health domains
- Auto-sync Garmin Connect + Hevy API
- OCR-parsing of analyzes via LLM
- Conflict engine between supplements and genes
- 7-day moving average + linear regression
- Plateau detection on GLP-1
</td>
<td width="50%">
**🤖 AI and Integrations**
- Weekly AI digests (Claude / GPT)
- 25 MCP tools for Claude.ai (read + write)
- OAuth 2.0 + PKCE authorization MCP
- Full data export for LLM (copy-paste ready)
- Atomic backup and database recovery
- Customizable models via OpenRouter
</td>
</tr>
<tr>
<td>
**📱 Mobile Experience**
- PWA with Home Screen installation
- Adaptation for iPhone / Android
- HTMX - instant transitions without reloads
- Alpine.js - micro-interactivity on the client
- Chart.js - adaptive charts
</td>
<td>
**🔒 Privacy and Control**
- Self-hosted, single-user, behind VPN
- Bcrypt authorization + signed sessions
- CSRF + Origin Guard + CSP
- Modular dashboard - enable/disable domains
- All data on your server
</td>
</tr>
</table>
---
### 📖 Contents
- [Philosophy and Architectural Principles](#philosophy-and-architectural-principles)
- [Data Domains (12 modules)](#data-domains)
- [System Architecture](#system-architecture)
- [MCP Integration with Claude.ai (25 tools)](#mcp-integration-with-claudeai)
- [Quick Start (Docker Compose)](#quick-start)
- [Secure Deployment (Author's Setup)](#secure-deployment)
- [Configuration Parameters (.env)](#configuration-parameters)
- [Development and Tests](#development-and-tests)
- [Support the Project](#support-the-project)
- [License](#license)
---
### 🧠 Philosophy and Key Principles
> [!NOTE]
> **1. Raw Data Preservation (Raw Payloads Store)**
> All integrations save original API responses in the `raw_payloads` JSONB table in parallel with structured records. If the parser updates tomorrow, the entire history will be re-parsed without loss.
> [!TIP]
> **2. Conflict Engine**
> Instead of strict validators - soft and hard warning rules (`conflict_rules`). Incompatible supplements with genetics or sudden weight spikes will return `409 Conflict`. You can click "Override" - the reason will be logged.
> [!IMPORTANT]
> **3. Localized Time (Timezone Engine)**
> "Today" and date boundaries are strictly in the user's timezone (`VITALS_TIMEZONE`). No messed up charts during evening workouts. All time through `vitals/utils/timeutils`.
> [!CAUTION]
> **4. Self-hosted & Single-User Privacy**
> Biometric data belongs to you only. Deploy on your own server behind VPN. Session cookies, bcrypt password, CSRF protection.
> [!TIP]
> **5. Full Data Portability**
> The system does not lock data. Full database backup (including raw API responses) for migration. Compact text export (LLM-ready) for insertion into a chat with Claude/ChatGPT. Atomic recovery from backup in one transaction.
### 📊 Data Domains
All 12 domains are united by a common interface `InsightsMixin` (`date`, `domain`, `source` + composite index):
<details>
<summary><strong>⚖️ 1. Body Weight and Composition</strong></summary>
- **Models**: `WeightLog`, `BodyMeasurement`, `NoiseMarker`, `ProgressPhoto`
- Calculation of body fat percentage using the Navy formula (neck, waist, hips) and lean body mass (LBM)
- 7-day moving average excluding noise periods (`NoiseMarker`)
- Linear regression: weight loss rate in kg/week and target date forecast
- Progress photos with date binding (batch loading up to 5 pieces)
- Manual weight overrides Garmin import for the same date (Garmin → `superseded`)
</details>
<details>
<summary><strong>💉 2. GLP-1 Protocol</strong></summary>
- **Models**: `Injection`, `DosePhase`, `SideEffect`
- Chronological log of injections (Semaglutide / Tirzepatide) with interactive map
- Linear scale of dosage phases for overlay on weight graphs
- Plateau detection: if dose >14 days and trend <100 g/week → automatic alert
</details>
<details>
<summary><strong>⌚ 3. Garmin Connect</strong></summary>
- **Models**: `GarminDaily`, `GarminActivity`
- Auto-sync: sleep, HRV, resting heart rate, stress, Body Battery, Training Readiness
- Garth session: tokens in Redis + backup to disk → minimum authorizations
- Backup channel: import JSON exports from Health Auto Export
- Display of last synchronization time
</details>
<details>
<summary><strong>🏋️ 4. Hevy Workouts</strong></summary>
- **Models**: `HevyWorkout`, `HevyExercise`, `HevySet`
- Synchronization of strength workouts via API (exercises, weights, sets, repetitions)
- Mapping of workout volume to Garmin recovery indicators
- Display of last synchronization time
</details>
<details>
<summary><strong>🍏 5. Nutrition</strong></summary>
- **Models**: `MealLog`
- Accounting for food intake with calories and macronutrients (P/F/C)
- Exact eating time (`eaten_at`) and text notes
- Customizable P/F/C goals via `.env` with progress visualization
- Automatic accounting in AI digests
</details>
<details>
<summary><strong>💊 6. Supplement Catalog</strong></summary>
- **Models**: `Supplement`
- Catalog of vitamins and nootropics: dosage, intake time, level of evidence (A/B/C)
- Conflict check with genetics and other supplements via Conflict Engine
</details>
<details>
<summary><strong>🧴 7. Skincare</strong></summary>
- **Models**: `SkincareProduct`, `SkincareLog`, `SkincareObservation`
- Morning/evening skincare diary with asset notes (acids, retinoids)
- Logging skin condition (oiliness, dryness, acne)
- Auto-alert for acid and retinoid conflicts
</details>
<details>
<summary><strong>🧪 8. Lab Tests and OCR</strong></summary>
- **Models**: `LabResult`, `LabMarker`
- PDF/photo upload → LLM parsing of biomarkers (recommended `gemini-2.5-flash`)
- Calculation of deviations (Low / High / Critical) and indicator change graphs
</details>
<details>
<summary><strong>🧬 9. Genetics (VCF)</strong></summary>
- **Models**: `GeneticVariant`
- VCF file parser → SNP mapping to predisposition catalog
- Data source for Conflict Engine in supplement prescription
</details>
<details>
<summary><strong>🎯 10. Goals and Milestones</strong></summary>
- **Models**: `Milestone`
- Interactive goal cards with target values and deadlines
- Dynamic calculation of completion percentage and remaining time forecast
</details>
<details>
<summary><strong>🤖 11. AI Digests</strong></summary>
- **Models**: `WeeklyDigest`
- Weekly LLM report on all domains (recommended `claude-sonnet-4.6`)
- Focus on connections: *“HRV dropped due to Hevy workout volume on GLP-1 plateau”*
</details>
<details>
<summary><strong>🔗 12. MCP Integration with Claude.ai</strong></summary>
- FastMCP server on `/mcp` (SSE transport)
- OAuth 2.0 + PKCE + Bearer tokens
- 25 tools: 14 for reading + 11 for writing data
- Details → [MCP section](#mcp-integration-with-claudeai)
</details>
---
### 🏗️ System Architecture
```mermaid
graph TD
Garmin[Garmin Connect / Garth] -->|auto-sync| Raw[raw_payloads · JSONB]
Hevy[Hevy REST API] -->|auto-sync| Raw
Labs[PDF analyses / Photos] -->|LLM parsing| Raw
VCF[Genetic VCF] -->|import| Raw
Raw --> CE[Conflict Engine]
CE --> IL[InsightsMixin Layer]
Scheduler[APScheduler] -->|Redis locks| CE
IL --> DB[(PostgreSQL 15)]
IL --> Cache[(Redis)]
DB & Cache --> FastAPI[FastAPI Router + Auth + CSRF]
FastAPI --> UI[Jinja2 + HTMX + Alpine.js]
FastAPI --> MCP[FastMCP SSE Server]
UI --> Charts[Chart.js]
UI --> AI[AI Digests · LLM]
MCP <-->|OAuth 2.0 + PKCE| Claude[Claude.ai]
```
| Layer | Responsibility |
| :--- | :--- |
| **`vitals/`** (core) | Models, services, business logic. Unaware of FastAPI. Imported in scripts and tests. |
| **`web/`** (delivery) | FastAPI routing, authorization, CSRF, Jinja2 templates. Calls services, does not contain business logic. |
| **Frontend** | HTML-over-the-wire: HTMX + Alpine.js + Chart.js. PWA-first, mobile-adapted. |
---
### 🔗 MCP Integration with Claude.ai
Built-in [FastMCP](https://github.com/jlowin/fastmcp) server available at `/mcp` (SSE transport). Authorization: OAuth 2.0 + PKCE with Bearer token verification. Configurable in the web interface (settings section).
**25 tools** — Claude can fully read and write data across all domains:
#### Reading (14 tools)
| Tool | Description |
| :--- | :--- |
| `get_user_profile` | Physical parameters, goals, workout program |
| `get_weight_logs` | Weight history, body measurements, noise periods |
| `get_glp1_logs` | GLP-1 injections, dosage phases, side effects |
| `get_garmin_metrics` | HRV, sleep, resting heart rate, activity |
| `get_hevy_workouts` | Strength workouts with sets and weights |
| `get_supplements_catalog` | Supplement catalog with dosages and evidence levels |
| `get_skincare_logs` | Skincare routine and skin condition observations |
| `get_lab_results` | Lab test results with biomarkers and references |
| `get_genetics_snps` | Genetic variants (rsid, genes) |
| `get_active_alerts` | Unresolved system alerts |
| `get_weekly_digests` | AI digest archive |
| `get_nutrition_summary` | Daily nutrition summary (P/F/C) and goal progress |
| `get_measurements` | Body measurements (neck, waist, hips, body fat percentage, LBM) |
| `get_notes` | Notes from all domains |
#### Writing (11 tools)
| Tool | Description |
| :--- | :--- |
| `log_meal` | Record meal intake with calories and macronutrients (P/F/C) |
| `update_meal` | Update existing meal record |
| `delete_meal` | Delete meal record |
| `search_meals` | Search meals by name and/or date |
| `log_weight` | Record weight (overrides Garmin for the same date) |
| `delete_weight` | Delete weight record (reactivate next by priority) |
| `log_glp1` | Record GLP-1 injection (medication, dose, site) |
| `log_skincare` | Record/update daily skincare checklist (upsert) |
| `log_measurement` | Record body measurements (auto-calculate Navy body fat percentage) |
| `log_note` | Add note to any domain record |
| `check_supplement_conflicts` | Check supplement conflicts with genes and other supplements |
#### Example Dialogue with Claude
```
User: How are my progress over the last 2 weeks? Weight, workouts, sleep — overall picture.
Claude: [calls get_weight_logs, get_hevy_workouts, get_garmin_metrics]
Weight: 89.2 → 87.8 kg in 14 days (−1.4 kg, ~0.7 kg/week — on track).
Workouts: 4 sessions (2× upper, 2× legs). Progression: bench press +5 kg,
lat pulldown +7.5 kg.
Sleep: average 7h 12min, HRV decreasing over last 3 days — likely,
accumulated fatigue after workouts. Body Battery recovering normally.
Recommendation: recovery sufficient, next workout can be conducted.
```
---
### 🚀 Quick Start
#### 1. Clone and Set Up Environment
```bash
git clone https://github.com/ilodezis/vitals.git
cd vitals
cp .env.example .env
```
#### 2. Generate Secrets
```bash
# Key for signing session cookies
python -c "import secrets; print(secrets.token_urlsafe(48))"
# Bcrypt hash for login password
python -c "import bcrypt; print(bcrypt.hashpw(b'your-password', bcrypt.gensalt(4)).decode())"
```
#### 3. Fill `.env`
```env
VITALS_SESSION_SECRET="result-of-first-command"
VITALS_AUTH_USERNAME="your-username"
VITALS_AUTH_PASSWORD_HASH="result-of-second-command"
```
#### 4. Run
```bash
docker compose up -d --build
```
> [!NOTE]
> Alembic migrations are applied automatically on `vitals_app` startup. PostgreSQL 15 — inside Docker network.
#### 5. Check
Dashboard: `http://127.0.0.1:8000`
```bash
curl -s http://127.0.0.1:8000/health
```
### 🛡️ Secure Deployment (Author's Setup)
Since Vitals contains sensitive medical, genetic, and personal biometric data, **it is strongly not recommended to leave the application open to the entire internet**. By default, in `docker-compose.yml`, ports are bound only to the local address (`127.0.0.1:8000`) so that no one from outside can access it directly.
#### Option A: Cloudflare Tunnel (Recommended by Author)
This is the easiest and most reliable way to publish the application with an SSL certificate without the need to open ports outward (your server remains invisible to port scanners).
1. **Domain Registration**: Add your domain to Cloudflare.
2. **Create a Tunnel**: In the [Cloudflare Zero Trust](https://one.dash.cloudflare.com/) panel, go to **Access -> Tunnels** and create a new tunnel (e.g., `vitals-tunnel`).
3. **Install cloudflared**: Copy the Cloudflare-provided installation command for your OS (Ubuntu/Debian) and execute it on your VPS. It will install and start the `cloudflared` daemon as a system service.
4. **Routing**: In the tunnel settings, in the **Public Hostname** tab, specify:
- **Subdomain/Domain**: e.g., `vitals.yourdomain.com`
- **Service Type**: `HTTP`
- **URL**: `localhost:8000` (or `127.0.0.1:8000`)
5. **Additional Protection (Cloudflare Access)**:
- To protect the application even before the authorization page, in the Zero Trust panel, go to **Access -> Applications** and create an application for your domain (e.g., `vitals.yourdomain.com`).
- Configure an Access Policy that allows entry only to your email address via One-Time PIN or social providers (Google/GitHub). Now, before logging into Vitals, you will need to confirm your identity with Cloudflare.
---
#### Option B: Classic Nginx Reverse Proxy + SSL + VPN/Basic Auth
A classic approach with the Nginx web server as a reverse proxy.
1. **DNS Setup**: Direct the A-record of the domain (e.g., `vitals.yourdomain.com`) to the public IP address of your VPS.
2. **Install Nginx**:
```bash
sudo apt update
sudo apt install nginx -y
```
3. **Host Configuration**: Create a file `/etc/nginx/sites-available/vitals` with the following content:
```nginx
server {
listen 80;
server_name vitals.yourdomain.com;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Disable size limit for uploaded PDF analyses and photos
client_max_body_size 20M;
}
}
```
Activate the config and restart Nginx:
```bash
sudo ln -s /etc/nginx/sites-available/vitals /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl restart nginx
```
4. **Install SSL Certificate (Let's Encrypt)**:
```bash
sudo apt install certbot python3-certbot-nginx -y
sudo certbot --nginx -d vitals.yourdomain.com
```
5. **Access Restriction (optional)**:
- **Via VPN (Tailscale/WireGuard)**: Configure Nginx to listen on port `443` only on the internal IP address of the VPN interface (e.g., `listen 100.x.x.x:443 ssl`), not on the external interface of the server.
- **Via HTTP Basic Auth**: Protect the route with a password. Generate a password file:
```bash
sudo apt install apache2-utils -y
sudo htpasswd -c /etc/nginx/.htpasswd username
```
Add to the `location /` block of the Nginx configuration:
```nginx
auth_basic "Private Vitals Access";
auth_basic_user_file /etc/nginx/.htpasswd;
```
---
#### Option C: Simply Hang on Your Domain (via Cloudflare Proxy)
The most standard way if you have a domain delegated to Cloudflare and want simple public access with SSL without additional VPN or authorization from Cloudflare Zero Trust.
1. **Cloudflare DNS Setup**:
- Create an A-record (e.g., `vitals.yourdomain.com`) pointing to the public IP address of your VPS.
- Ensure that the proxy mode is enabled (**Proxied** / orange cloud). This will hide the real IP address of your VPS from the outside world and provide built-in protection against DDoS.
2. **Cloudflare SSL/TLS Setup**:
- In the Cloudflare control panel, go to **SSL/TLS -> Overview**.
- Set the encryption mode to **Full** or **Full (strict)**.
3. **Install Nginx on VPS**:
```bash
sudo apt update
sudo apt install nginx -y
```
4. **Nginx Configuration**:
Create a file `/etc/nginx/sites-available/vitals` with traffic proxied to the local port 8000:
```nginx
server {
listen 80;
server_name vitals.yourdomain.com;
client_max_body_size 20M;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
```
Activate the configuration and restart Nginx:
```bash
sudo ln -s /etc/nginx/sites-available/vitals /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl restart nginx
```
5. **SSL Certificates (choose one)**:
- **Origin Certificate from Cloudflare** (Recommended):
- In the Cloudflare panel, go to **SSL/TLS -> Origin Server** and click **Create Certificate**.
- Copy the contents of the private key and certificate and save them on the VPS (e.g., in `/etc/ssl/private/cloudflare.key` and `/etc/ssl/certs/cloudflare.pem`).
- Update the Nginx configuration, switching it to port 443 using these certificates:
```nginx
server {
listen 80;
server_name vitals.yourdomain.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name vitals.yourdomain.com;
ssl_certificate /etc/ssl/certs/cloudflare.pem;
ssl_certificate_key /etc/ssl/private/cloudflare.key;
client_max_body_size 20M;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
```
Restart Nginx: `sudo systemctl restart nginx`.
- **Use Let's Encrypt**:
- If you do not want to use Cloudflare Origin certificates, simply obtain a standard Let's Encrypt certificate through Certbot:
```bash
sudo apt install certbot python3-certbot-nginx -y
sudo certbot --nginx -d vitals.yourdomain.com
```
### ⚙️ Configuration Parameters
<details>
<summary><strong>Infrastructure</strong></summary>
| Variable | Description | Default |
| :--- | :--- | :--- |
| `VITALS_DATABASE_URL` | PostgreSQL (asyncpg) | `postgresql+asyncpg://...` |
| `VITALS_REDIS_URL` | Redis for cache and locks | `redis://vitals_redis:6379/0` |
| `VITALS_TIMEZONE` | User timezone | `Europe/Chisinau` |
</details>
<details>
<summary><strong>Authorization and Security</strong></summary>
| Variable | Description | Default |
| :--- | :--- | :--- |
| `VITALS_SESSION_SECRET` | Session signature key | *Required* |
| `VITALS_AUTH_USERNAME` | Username | *Required* |
| `VITALS_AUTH_PASSWORD_HASH` | Bcrypt password hash | *Required* |
| `VITALS_SESSION_TTL` | Session lifetime (sec) | `2592000` (30 days) |
| `VITALS_COOKIE_SECURE` | Secure flag for cookies | `true` |
| `VITALS_COOKIE_SAMESITE` | SameSite policy | `lax` |
</details>
<details>
<summary><strong>User Profile</strong></summary>
| Variable | Description | Default |
| :--- | :--- | :--- |
| `VITALS_HEIGHT_CM` | Height (cm) | `175` |
| `VITALS_SEX` | Sex (`male` / `female`) | `male` |
| `VITALS_USER_AGE` | Age | `25` |
| `VITALS_USER_PROGRAM` | Program description | *Built-in* |
| `VITALS_USER_GOALS` | Goals (comma-separated) | `fat loss, muscle preservation` |
</details>
<details>
<summary><strong>Nutrition Goals</strong></summary>
| Variable | Description | Default |
| :--- | :--- | :--- |
| `VITALS_NUTRITION_PROTEIN_TARGET_G` | Daily protein target (g) | `150` |
| `VITALS_NUTRITION_CALORIES_MIN` | Minimum calories | `1300` |
| `VITALS_NUTRITION_CALORIES_MAX` | Maximum calories | `1700` |
</details>
<details>
<summary><strong>Integrations and AI</strong></summary>
| Variable | Description | Default |
| :--- | :--- | :--- |
| `VITALS_OPENROUTER_API_KEY` | OpenRouter API key | *Optional* |
| `VITALS_LLM_MODEL_DIGEST` | Model for digests | `anthropic/claude-sonnet-4.6` |
| `VITALS_LLM_MODEL_PARSER` | Model for OCR analysis | `google/gemini-2.5-flash` |
| `VITALS_HEVY_API_KEY` | Hevy API key | *Optional* |
| `VITALS_GARMIN_EMAIL` | Garmin Connect email | *Optional* |
| `VITALS_GARMIN_PASSWORD` | Garmin Connect password | *Optional* |
| `VITALS_GARMIN_TOKEN_DIR` | Path to Garmin tokens | `/data/garmin_session` |
| `VITALS_MCP_CLIENT_ID` | OAuth Client ID for MCP | `vitals-claude-connector` |
| `VITALS_MCP_CLIENT_SECRET` | OAuth Client Secret for MCP | *Required* |
</details>
---
### 🛠️ Development and Testing
#### Local Environment
```bash
python -m venv .venv
.venv\Scripts\activate # Windows
source .venv/bin/activate # macOS/Linux
pip install -r requirements-dev.txt
```
#### Dev Server (without Docker)
```bash
python run_local.py
```
Automatically sets up SQLite + FakeRedis. Default login: `timur` / `password`. Address: `http://127.0.0.1:8000`.
#### Demo Data
```bash
python scripts/seed_demo.py
```
Seeds the local database with a plausible set of data across all domains (weight, Garmin, workouts, nutrition, GLP-1, lab reports, supplements, genetics, skincare). Idempotent — can be run repeatedly.
#### Tests
```bash
# Unit tests (SQLite, instant)
python -m pytest -q
# Integration tests (Postgres Docker)
bash scripts/test_postgres.sh
```
---
### ☕ Support the Project
> [!TIP]
> If **Vitals** helps you track your health metrics and makes your life easier, you can support the project:
>
> * **Russian Federation (CloudTips)**: [pay.cloudtips.ru/p/5bb82648](https://pay.cloudtips.ru/p/5bb82648)
> * **International cards (Ko-fi)**: [ko-fi.com/ilodezis](https://ko-fi.com/ilodezis)
> * **USDT (TRC-20)**: `TV6bMburcEjgkinkZBvt9DonkyiaP7TqHG`
> * **USDT (Arbitrum One)**: `0x3eac15f5d07bba100d4038cad603e420a84753bb`
---
### 📄 License
**PolyForm Noncommercial License 1.0.0** — usage and modification are permitted for non-commercial purposes. Commercial use is prohibited. Details: [LICENSE](LICENSE).
---
---
## Vitals - Personal Health Dashboard
**Vitals** is a personal health dashboard and data lake designed for a single user. Built for long-term tracking of biomarkers, body recomposition, GLP-1 therapy, athletic performance, and AI-powered weekly analytical digests via OpenRouter LLMs.
Unlike typical fitness trackers, Vitals prioritizes **preserving raw historical data**. It serves as a smart wellness navigator — uncovering correlations between sleep, workouts, supplements, and body composition. Not a watchdog — a co-pilot.
### Why I built this
My data was scattered across six apps: Garmin, Hevy, GLP-1 notes, a spreadsheet with lab results, a file with supplements. None of them could cross-reference sleep with training recovery or link therapy side effects to nutrition — I had to keep it all in my head.
Built with Claude as the primary coding tool — but the data model, architecture, and domain design are mine. The MCP layer is the key part: Claude can read and write data across all 12 domains in real time, answering questions like "how did my sleep this week affect my recovery?" with actual numbers.
<p align="center">
<img src="./gifs/MCP_en.gif" alt="Claude querying Vitals via MCP" width="100%">
</p>
### What it does
<p align="center">
<img src="./gifs/dashboard_en.gif" alt="Vitals dashboard walkthrough" width="100%">
</p>
<table>
<tr>
<td width="50%">
**📊 Data & Analytics**
- 12 independent health domains
- Auto-sync Garmin Connect + Hevy API
- OCR lab report parsing via LLM
- Conflict engine for supplements vs. genetics
- 7-day moving average + linear regression
- GLP-1 plateau detection
</td>
<td width="50%">
**🤖 AI & Integrations**
- Weekly AI digests (Claude / GPT via OpenRouter)
- 25 MCP tools for Claude.ai (read + write)
- OAuth 2.0 + PKCE authorization for MCP
- Full data export for LLM (copy-paste ready)
- Atomic database backup & restore
- Configurable models via OpenRouter
</td>
</tr>
<tr>
<td>
**📱 Mobile Experience**
- PWA with Home Screen install
- iPhone / Android adapted
- HTMX — instant transitions, no page reloads
- Alpine.js — client-side micro-interactivity
- Chart.js — responsive charts
</td>
<td>
**🔒 Privacy & Control**
- Self-hosted, single-user, behind VPN
- Bcrypt auth + signed session cookies
- CSRF + Origin Guard + CSP
- Modular dashboard — toggle domains on/off
- All data stays on your server
</td>
</tr>
</table>
---
### 📖 Table of Contents
- [Core Philosophy](#-core-philosophy)
- [Supported Domains (12 modules)](#-supported-domains)
- [Technical Architecture](#-technical-architecture)
- [MCP Integration with Claude.ai (25 tools)](#-mcp-integration-with-claudeai)
- [Quick Start (Docker Compose)](#-quick-start)
- [Secure Deployment (Creator's Setup)](#-secure-deployment-creators-setup)
- [Configuration (.env)](#-configuration)
- [Development & Testing](#-development--testing)
- [Support the Project](#-support-the-project)
- [License](#-license)
---
### 🧠 Core Philosophy
> [!NOTE]
> **1. Raw Data Preservation**
> All API integrations preserve original JSON payloads in a JSONB `raw_payloads` table alongside normalized records. If a parser changes — the entire history can be re-processed with zero loss.
> [!TIP]
> **2. Resilient Conflict Engine**
> Instead of hard validations — soft and hard warning rules (`conflict_rules`). A supplement clashing with genetics or an abnormal weight spike returns `409 Conflict`. Users can explicitly override with an audit note.
> [!IMPORTANT]
> **3. Localized Time Boundaries**
> Daily resets and calendar math resolve against the user's timezone (`VITALS_TIMEZONE`), not the server clock. No more split days from late-night workouts. All time via `vitals/utils/timeutils`.
> [!CAUTION]
> **4. Single-User Self-Hosted Privacy**
> Your biometrics belong to you. Designed for deployment behind a VPN. Secure session cookies, bcrypt passwords, CSRF protection.
> [!TIP]
> **5. End-to-End Data Portability**
> Full database backup (including raw API payloads) for migration. Curated, secret-free text export designed for pasting into LLM chats (Claude, ChatGPT). Atomic restore within a single database transaction.
---
### 📊 Supported Domains
All 12 domains share the `InsightsMixin` interface (`date`, `domain`, `source` + composite index):
<details>
<summary><strong>⚖️ 1. Weight & Body Composition</strong></summary>
- **Models**: `WeightLog`, `BodyMeasurement`, `NoiseMarker`, `ProgressPhoto`
- US Navy body fat % formula (neck, waist, hips) + Lean Body Mass (LBM)
- 7-day moving average with noise period exclusion (`NoiseMarker`)
- Linear regression: weekly rate (kg/week) + goal date projection
- Progress photos with calendar linking (batch upload up to 5 at once)
- Manual logs override Garmin imports for the same date (Garmin → `superseded`)
</details>
<details>
<summary><strong>💉 2. GLP-1 Protocol</strong></summary>
- **Models**: `Injection`, `DosePhase`, `SideEffect`
- Chronological injection log (Semaglutide / Tirzepatide) with interactive site map
- Dose phase overlays on weight charts for correlation
- Plateau detection: dose >14 days + trend <100g/week → automatic warning
</details>
<details>
<summary><strong>⌚ 3. Garmin Connect</strong></summary>
- **Models**: `GarminDaily`, `GarminActivity`
- Auto-sync: sleep, HRV, resting HR, stress, Body Battery, Training Readiness
- Garth session tokens cached in Redis + disk backup → minimal re-auth
- Fallback: direct Health Auto Export JSON import
- Last sync timestamp display
</details>
<details>
<summary><strong>🏋️ 4. Hevy Workouts</strong></summary>
- **Models**: `HevyWorkout`, `HevyExercise`, `HevySet`
- API sync: exercises, sets, reps, weight
- Cross-reference training volume with Garmin recovery metrics
- Last sync timestamp display
</details>
<details>
<summary><strong>🍏 5. Nutrition</strong></summary>
- **Models**: `MealLog`
- Meal logging with calories and macros (protein, fat, carbs)
- Wall-clock consumption time (`eaten_at`) + text notes
- Configurable macro targets via `.env` with dashboard progress gauges
- Auto-included in weekly AI digests
</details>
<details>
<summary><strong>💊 6. Supplements Catalog</strong></summary>
- **Models**: `Supplement`
- Supplement database with dosages, timing, evidence tiers (A/B/C)
- Conflict checks against genetics and concurrent substances via Conflict Engine
</details>
<details>
<summary><strong>🧴 7. Skincare Log</strong></summary>
- **Models**: `SkincareProduct`, `SkincareLog`, `SkincareObservation`
- Morning / evening routine logs with product actives (retinol, peels)
- Skin status tracking (hydration, oiliness, breakouts)
- Auto-warning for conflicting actives
</details>
<details>
<summary><strong>🧪 8. Lab Results & OCR</strong></summary>
- **Models**: `LabResult`, `LabMarker`
- Upload PDF / image → LLM parses biomarkers (recommended: `gemini-2.5-flash`)
- Out-of-range flagging (Low / High / Critical) + interactive history charts
</details>
<details>
<summary><strong>🧬 9. Genetics (VCF)</strong></summary>
- **Models**: `GeneticVariant`
- VCF file parser → health-relevant SNPs (caffeine, iron, lactose)
- Feeds into Conflict Engine for supplement safety checks
</details>
<details>
<summary><strong>🎯 10. Milestones & Goals</strong></summary>
- **Models**: `Milestone`
- Interactive goal cards with numeric targets and deadlines
- Real-time progress % based on active weight metrics
</details>
<details>
<summary><strong>🤖 11. Weekly AI Digests</strong></summary>
- **Models**: `WeeklyDigest`
- Weekly LLM digest across all domains (recommended: `claude-sonnet-4.6`)
- Focused on causal links: *"HRV recovered as training volume decreased, breaking the weight stall on the current GLP-1 phase"*
</details>
<details>
<summary><strong>🔗 12. Claude.ai MCP Integration</strong></summary>
- FastMCP server at `/mcp` (SSE transport)
- OAuth 2.0 + PKCE + Bearer tokens
- 25 tools: 14 read + 11 write
- Details → [MCP section](#-mcp-integration-with-claudeai)
</details>
---
### 🏗️ Technical Architecture
```mermaid
graph TD
Garmin[Garmin Connect / Garth] -->|auto-sync| Raw[raw_payloads · JSONB]
Hevy[Hevy REST API] -->|auto-sync| Raw
Labs[Lab PDFs / Images] -->|LLM parsing| Raw
VCF[Genetics VCF] -->|import| Raw
Raw --> CE[Conflict Engine]
CE --> IL[InsightsMixin Layer]
Scheduler[APScheduler] -->|Redis locks| CE
IL --> DB[(PostgreSQL 15)]
IL --> Cache[(Redis)]
DB & Cache --> FastAPI[FastAPI Router + Auth + CSRF]
FastAPI --> UI[Jinja2 + HTMX + Alpine.js]
FastAPI --> MCP[FastMCP SSE Server]
UI --> Charts[Chart.js]
UI --> AI[AI Digests · LLM]
MCP <-->|OAuth 2.0 + PKCE| Claude[Claude.ai]
```
| Layer | Responsibility |
| :--- | :--- |
| **`vitals/`** (core) | Models, services, business logic. Zero web dependencies. Importable in scripts and tests. |
| **`web/`** (delivery) | FastAPI routing, auth, CSRF, Jinja2 templates. Calls services — contains no business logic. |
| **Frontend** | HTML-over-the-wire: HTMX + Alpine.js + Chart.js. PWA-first, mobile-adapted. |
---
### 🔗 MCP Integration with Claude.ai
Built-in [FastMCP](https://github.com/jlowin/fastmcp) server at `/mcp` (SSE transport). Authorization: OAuth 2.0 + PKCE with signed Bearer token verification. Configured in the web dashboard settings.
**25 tools** — Claude can fully read and write data across all domains:
#### Read (14 tools)
| Tool | Description |
| :--- | :--- |
| `get_user_profile` | Physical profile, goals, training program |
| `get_weight_logs` | Weight history, body measurements, noise markers |
| `get_glp1_logs` | GLP-1 injections, dose phases, side effects |
| `get_garmin_metrics` | HRV, sleep, resting HR, activities |
| `get_hevy_workouts` | Strength workouts with sets, reps, weights |
| `get_supplements_catalog` | Supplements with dosages and evidence tiers |
| `get_skincare_logs` | Skincare routine logs and skin observations |
| `get_lab_results` | Lab reports with biomarkers and reference ranges |
| `get_genetics_snps` | Genetic SNPs (rsid, genes) |
| `get_active_alerts` | Unresolved warnings and conflict notifications |
| `get_weekly_digests` | Historical AI digest archive |
| `get_nutrition_summary` | Daily nutrition summary (calories, macros) and goal progress |
| `get_measurements` | Body measurements (neck, waist, hips, body-fat %, LBM) |
| `get_notes` | Records with notes across all domains |
#### Write (11 tools)
| Tool | Description |
| :--- | :--- |
| `log_meal` | Record a meal with calories and macros |
| `update_meal` | Update an existing meal entry |
| `delete_meal` | Delete a meal entry |
| `search_meals` | Search meals by name and/or date |
| `log_weight` | Record weight (overrides Garmin for the same date) |
| `delete_weight` | Delete a weight entry (reactivates next priority) |
| `log_glp1` | Record a GLP-1 injection (drug, dose, site) |
| `log_skincare` | Record/update daily skincare checklist (upsert) |
| `log_measurement` | Record body measurements (auto-computes Navy body-fat %) |
| `log_note` | Add a note to any domain record |
| `check_supplement_conflicts` | Check a supplement against genetics and active supplements |
#### Example interaction
```
User: Summarize my last 2 weeks — weight trend, workouts, sleep quality.
Claude: [calls get_weight_logs, get_hevy_workouts, get_garmin_metrics]
Weight: 89.2 → 87.8 kg over 14 days (−1.4 kg, ~0.7 kg/week — on track).
Workouts: 4 sessions completed (2× upper, 2× lower). Progressive overload:
bench press +5 kg, lat pulldown +7.5 kg.
Sleep: avg 7h 12min, HRV trending down last 3 days — likely accumulated
post-training fatigue. Body Battery recovering normally.
Recommendation: recovery looks sufficient, next session is fine to proceed.
```
---
### 🚀 Quick Start
#### 1. Clone and configure
```bash
git clone https://github.com/ilodezis/vitals.git
cd vitals
cp .env.example .env
```
#### 2. Generate secrets
```bash
# Session cookie signing key
python -c "import secrets; print(secrets.token_urlsafe(48))"
# Bcrypt password hash
python -c "import bcrypt; print(bcrypt.hashpw(b'your-password', bcrypt.gensalt(4)).decode())"
```
#### 3. Update `.env`
```env
VITALS_SESSION_SECRET="your-session-secret"
VITALS_AUTH_USERNAME="your-username"
VITALS_AUTH_PASSWORD_HASH="your-bcrypt-hash"
```
#### 4. Launch
```bash
docker compose up -d --build
```
> [!NOTE]
> Alembic migrations run automatically when `vitals_app` starts. PostgreSQL 15 runs inside the Docker network.
#### 5. Verify
Dashboard: `http://127.0.0.1:8000`
```bash
curl -s http://127.0.0.1:8000/health
```
---
### 🛡️ Secure Deployment (Creator's Setup)
Since Vitals stores sensitive medical history, genetic profiles, and personal biometric logs, **it is strongly advised not to leave the application exposed to the public internet**. By default, the port mapping in `docker-compose.yml` binds only to the loopback interface (`127.0.0.1:8000`), keeping the application hidden from outside traffic.
The project creator deploys Vitals on an **Ubuntu VPS** and secures access using one of three methods:
#### Option A: Cloudflare Tunnel (Recommended)
This is the simplest and most secure method to publish the app with a valid SSL certificate without exposing any ports on your firewall or router (your server's IP remains hidden).
1. **Add Domain**: Set up your domain in Cloudflare.
2. **Create Tunnel**: In the [Cloudflare Zero Trust Dashboard](https://one.dash.cloudflare.com/), navigate to **Access -> Tunnels** and create a new tunnel (e.g., `vitals-tunnel`).
3. **Install cloudflared**: Copy the installation command provided by Cloudflare for your server's OS (Ubuntu/Debian) and run it on the VPS. This installs the `cloudflared` daemon as a system service.
4. **Route Traffic**: In the tunnel's configuration under the **Public Hostname** tab, add:
- **Subdomain/Domain**: e.g., `vitals.yourdomain.com`
- **Service Type**: `HTTP`
- **URL**: `localhost:8000` (or `127.0.0.1:8000`)
5. **Additional Protection (Cloudflare Access)**:
- To secure the app before the user even sees the Vitals login screen, go to **Access -> Applications** in the Zero Trust dashboard and add a self-hosted application for your domain (e.g., `vitals.yourdomain.com`).
- Define an Access Policy allowing access only to your email address via One-Time PINs (OTP) or authentication providers like Google/GitHub.
---
#### Option B: Classic Nginx Reverse Proxy + SSL + VPN/Basic Auth
A traditional setup using Nginx as a reverse proxy.
1. **DNS Setup**: Point your domain's A-record (e.g., `vitals.yourdomain.com`) to your VPS's public IP.
2. **Install Nginx**:
```bash
sudo apt update
sudo apt install nginx -y
```
3. **Configure virtual host**: Create a file at `/etc/nginx/sites-available/vitals` with the following configuration:
```nginx
server {
listen 80;
server_name vitals.yourdomain.com;
# Allow uploading larger lab reports and photos
client_max_body_size 20M;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
```
Activate the configuration and restart Nginx:
```bash
sudo ln -s /etc/nginx/sites-available/vitals /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl restart nginx
```
4. **Provision SSL Certificate (Let's Encrypt)**:
```bash
sudo apt install certbot python3-certbot-nginx -y
sudo certbot --nginx -d vitals.yourdomain.com
```
5. **Restrict Access (Pick one)**:
- **Via VPN (Tailscale/WireGuard)**: Configure Nginx to listen for SSL traffic only on your private VPN IP interface (e.g., `listen 100.x.x.x:443 ssl`), rather than the public interface.
- **Via HTTP Basic Auth**: Generate a password file:
```bash
sudo apt install apache2-utils -y
sudo htpasswd -c /etc/nginx/.htpasswd username
```
Add the following directives to your Nginx `location /` block:
```nginx
auth_basic "Private Vitals Access";
auth_basic_user_file /etc/nginx/.htpasswd;
```
---
#### Option C: Simple Public Domain (via Cloudflare Proxy)
The standard path if you want direct public access to your Vitals dashboard using a custom domain managed by Cloudflare, with SSL handled by Cloudflare's edge servers.
1. **DNS Settings in Cloudflare**:
- Add an A record (e.g., `vitals.yourdomain.com`) pointing to your VPS's public IP address.
- Ensure the proxy status is set to **Proxied** (orange cloud). This hides your VPS's public IP and shields it from direct traffic/DDoS attacks.
2. **SSL/TLS Settings in Cloudflare**:
- In the Cloudflare Dashboard, go to **SSL/TLS -> Overview**.
- Change the encryption mode to **Full** or **Full (strict)**.
3. **Install Nginx on the VPS**:
```bash
sudo apt update
sudo apt install nginx -y
```
4. **Configure virtual host**:
Create a configuration file at `/etc/nginx/sites-available/vitals` to forward traffic to the local port 8000:
```nginx
server {
listen 80;
server_name vitals.yourdomain.com;
client_max_body_size 20M;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
```
Activate the configuration and restart Nginx:
```bash
sudo ln -s /etc/nginx/sites-available/vitals /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl restart nginx
```
5. **SSL Certificates (Choose one)**:
- **Cloudflare Origin Certificate** (Recommended):
- In the Cloudflare Dashboard, go to **SSL/TLS -> Origin Server** and click **Create Certificate**.
- Copy the certificate and private key and save them on your VPS (e.g., to `/etc/ssl/certs/cloudflare.pem` and `/etc/ssl/private/cloudflare.key`).
- Update your Nginx configuration file to listen on port 443 with these certificates:
```nginx
server {
listen 80;
server_name vitals.yourdomain.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name vitals.yourdomain.com;
ssl_certificate /etc/ssl/certs/cloudflare.pem;
ssl_certificate_key /etc/ssl/private/cloudflare.key;
client_max_body_size 20M;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
```
Restart Nginx: `sudo systemctl restart nginx`.
- **Let's Encrypt / Certbot**:
- Alternatively, obtain a standard Let's Encrypt certificate:
```bash
sudo apt install certbot python3-certbot-nginx -y
sudo certbot --nginx -d vitals.yourdomain.com
```
---
### ⚙️ Configuration
<details>
<summary><strong>Infrastructure</strong></summary>
| Variable | Description | Default |
| :--- | :--- | :--- |
| `VITALS_DATABASE_URL` | PostgreSQL (asyncpg) | `postgresql+asyncpg://...` |
| `VITALS_REDIS_URL` | Redis for cache and locks | `redis://vitals_redis:6379/0` |
| `VITALS_TIMEZONE` | User timezone | `Europe/Chisinau` |
</details>
<details>
<summary><strong>Authentication & Security</strong></summary>
| Variable | Description | Default |
| :--- | :--- | :--- |
| `VITALS_SESSION_SECRET` | Session cookie signing key | *Required* |
| `VITALS_AUTH_USERNAME` | Dashboard username | *Required* |
| `VITALS_AUTH_PASSWORD_HASH` | Bcrypt password hash | *Required* |
| `VITALS_SESSION_TTL` | Session duration (seconds) | `2592000` (30 days) |
| `VITALS_COOKIE_SECURE` | Secure cookie flag | `true` |
| `VITALS_COOKIE_SAMESITE` | SameSite policy | `lax` |
</details>
<details>
<summary><strong>User Profile</strong></summary>
| Variable | Description | Default |
| :--- | :--- | :--- |
| `VITALS_HEIGHT_CM` | Height (cm) | `175` |
| `VITALS_SEX` | Sex (`male` / `female`) | `male` |
| `VITALS_USER_AGE` | Age | `25` |
| `VITALS_USER_PROGRAM` | Program description | *Embedded* |
| `VITALS_USER_GOALS` | Goals (comma-separated) | |
</details>
<details>
<summary><strong>Nutrition Targets</strong></summary>
| Variable | Description | Default |
| :--- | :--- | :--- |
| `VITALS_NUTRITION_PROTEIN_TARGET_G` | Daily protein target (g) | `150` |
| `VITALS_NUTRITION_CALORIES_MIN` | Minimum daily kcal | `1300` |
| `VITALS_NUTRITION_CALORIES_MAX` | Maximum daily kcal | `1700` |
</details>
<details>
<summary><strong>Integrations & AI</strong></summary>
| Variable | Description | Default |
| :--- | :--- | :--- |
| `VITALS_OPENROUTER_API_KEY` | OpenRouter API key | *Optional* |
| `VITALS_LLM_MODEL_DIGEST` | Digest model | `anthropic/claude-sonnet-4.6` |
| `VITALS_LLM_MODEL_PARSER` | Lab OCR model | `google/gemini-2.5-flash` |
| `VITALS_HEVY_API_KEY` | Hevy API key | *Optional* |
| `VITALS_GARMIN_EMAIL` | Garmin Connect email | *Optional* |
| `VITALS_GARMIN_PASSWORD` | Garmin Connect password | *Optional* |
| `VITALS_GARMIN_TOKEN_DIR` | Garmin session token path | `/data/garmin_session` |
| `VITALS_MCP_CLIENT_ID` | MCP OAuth Client ID | `vitals-claude-connector` |
| `VITALS_MCP_CLIENT_SECRET` | MCP OAuth Client Secret | *Required* |
</details>
---
### 🛠️ Development & Testing
#### Local environment
```bash
python -m venv .venv
source .venv/bin/activate # macOS/Linux
.venv\Scripts\activate # Windows
pip install -r requirements-dev.txt
```
#### Dev server (no Docker)
```bash
python run_local.py
```
Auto-provisions SQLite + FakeRedis. Default login: `timur` / `password`. Address: `http://127.0.0.1:8000`.
Demo Data
```bash
python scripts_demo.py
```
Populates the with realistic data across all domains (weight, Garmin, workouts, nutrition, GLP-1, labs, supplements, genetics, skincare). Idempotent — safe to re-run.
#### Tests
```bash
# Unit tests (SQLite, instant)
python -m pytest -q
# IntegrationPostgres Docker)
bash scripts/test_postgres.sh
```
---
the Project
> [!TIP]
> If **Vitals** helps you track your health markers and makes better, consider supporting its development. toward keeping the creator caffeinated:
>
> * **Russian Cards (CloudTips)**: [pay.cloudtips.ru/p/5bb82648](https://pay.cloudtips.ru/p/5bb82648)
> * **International Cards (Ko-fi)**: [ko-fi.com/ilodezis](https://ko-fi.com/ilodezis)
> * **USDT (TRC-20)**: `TV6bMburcEjgkinkZBvt9DonkyiaP7TqHG`
**USDT (Arbitrum One)**: `0x315f5d07bba100d4038cad603e420a84753bb`
---
### License
**PolyForm Noncommercial License 1.0.0** — free for personal and private use. Commercial use prohibited. See [LICENSE](LICENSE).
MCP Config
Below is the configuration for this MCP Server. You can copy it directly to Cursor or other MCP clients.
mcp.json
Connection Info
You Might Also Like
No recommendations available