diff --git a/.env.example b/.env.example index 4b0fdbf..8081ddb 100644 --- a/.env.example +++ b/.env.example @@ -1,19 +1,23 @@ -# === REQUIRED (generated automatically by setup.sh) === -SECRET_KEY= +# Diligence Configuration +# Copy this to .env and fill in values, or run ./setup.sh to auto-generate. + +# Required — auto-generated by setup.sh +SECRET_KEY=change-me-in-production API_TOKEN= -# === APP URL (change for production) === -BASE_URL=http://localhost +# Database — leave empty for SQLite (pip install path) +# Set for PostgreSQL (Docker path): postgresql+asyncpg://fitness@fitness-db:5432/fitness_rewards +DATABASE_URL= -# === DATABASE (defaults work out of the box) === -DB_USER=fitness -DB_NAME=fitness_rewards +# App +BASE_URL=http://localhost:8000 -# === MCP AGENT AUTH (generated automatically by setup.sh) === -# The API_TOKEN authenticates the MCP connector with the backend. -# It is auto-configured — you don't need to touch it unless you're -# connecting an agent from outside Docker. - -# Everything else — Strava, Polar, Garmin, Fitbit, Telegram, -# USDA, Groq, etc. — is configured through the app UI or your AI agent. -# No container restart needed. +# Optional integrations +STRAVA_CLIENT_ID= +STRAVA_CLIENT_SECRET= +POLAR_CLIENT_ID= +POLAR_CLIENT_SECRET= +GROQ_API_KEY= +TELEGRAM_BOT_TOKEN= +TELEGRAM_CHAT_ID= +CRAWL_ENABLED=false diff --git a/.gitignore b/.gitignore index cc1fa53..9a86dd0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,31 @@ +# Environment +.env +*.env.local + +# Python __pycache__/ *.py[cod] *.egg-info/ -.env -.venv/ -node_modules/ dist/ -.DS_Store -*.log +build/ +*.egg -# Excluded from open-source (scraped third-party content) +# Node +frontend/node_modules/ +frontend/dist/ + +# Data +*.db +*.sqlite +*.sqlite3 + +# IDE +.vscode/ +.idea/ + +# OS +.DS_Store +Thumbs.db + +# Content (scraped recipes — not for distribution) content/ diff --git a/README.md b/README.md index 37fb408..7094339 100644 --- a/README.md +++ b/README.md @@ -1,237 +1,211 @@ -# Diligence — Self-Hosted Fitness Rewards with AI Agent Support +# Diligence -Points-based fitness accountability app. Log workouts and food, earn points, unlock rewards. Connect any AI agent via MCP for logging, coaching, and meal planning. Self-hosted, open source, zero cloud dependencies. +Self-hosted fitness rewards platform with AI agent integration. Points-based behavioral economy: earn points through workouts and food logging, spend them on rewards you choose. Science-based onboarding. 14-tool MCP connector for AI agents. Your data stays on your machine. -Built by [DiligenceWorks](https://diligenceworks.online). - -## Quick Start - -### Prerequisites - -- [Docker Desktop](https://www.docker.com/products/docker-desktop/) — that's it. No Python, Node.js, or database install needed. -- Docker Desktop runs natively on **Windows 10/11**, **macOS**, and **Linux**. +**By [DiligenceWorks Pte. Ltd.](https://diligenceworks.online) — MIT License** --- -### Windows 11 Setup +## Which install is right for you? -**1. Install Docker Desktop** +| | **pip install** | **Docker** | +|---|---|---| +| **Who it's for** | You want a fitness app on your laptop. No servers, no DevOps, no fuss. | You're self-hosting for a household, a team, or you want PostgreSQL and a production-grade setup. | +| **What you need** | Python 3.11+ | Docker and Docker Compose | +| **Database** | SQLite (zero config, file on disk) | PostgreSQL 16 (runs in a container) | +| **Runs as** | Single process on localhost | 4 containers behind nginx | +| **Setup time** | 2 minutes | 5 minutes | +| **Best for** | Personal use on a laptop or desktop | Always-on server, multiple users, backups | -Download from [docker.com/products/docker-desktop](https://www.docker.com/products/docker-desktop/). +--- -During installation, the installer presents a checkbox: **"Use WSL 2 instead of Hyper-V (recommended)"**. This is checked by default. +## Install — Personal Laptop (pip) -- **WSL 2 backend (default):** Works on all Windows editions including Home. Lower resource usage, better performance. Requires WSL 2 to be installed first — open PowerShell as Administrator and run `wsl --install`, then reboot. -- **Hyper-V backend:** If WSL 2 causes issues (black screen on reboot, kernel errors, or containers won't start), uncheck the WSL 2 box during installation to use Hyper-V instead. Requires Windows Pro, Enterprise, or Education. +**Prerequisites:** Python 3.11 or newer. -**Important:** Hardware virtualization must be enabled in your BIOS/UEFI settings (Intel VT-x or AMD-V). This is the most common cause of "Docker won't start" issues. Check your laptop/PC manufacturer's documentation for how to access BIOS settings (usually F2, F12, or Del during boot). +```bash +# Clone and install +git clone https://github.com/DiligenceWorks/Diligence.git +cd Diligence +pip install . -**2. Clone and run** - -Open PowerShell: -```powershell -git clone https://github.com/diligenceworks/diligence -cd diligence -powershell -ExecutionPolicy Bypass -File setup.ps1 -docker compose up -d +# Run +diligence ``` -Or without the script: -```powershell -git clone https://github.com/diligenceworks/diligence -cd diligence -copy .env.example .env -# Edit .env and set SECRET_KEY to any random string -docker compose up -d +That's it. The app opens in your browser at `http://localhost:8000`. Your data lives in `~/.diligence/`. + +On first run, Diligence generates a secret key and stores your config in `~/.diligence/.env`. The SQLite database is created automatically at `~/.diligence/data.db`. + +### Options + +```bash +diligence --port 9000 # Different port +diligence --no-browser # Don't auto-open browser +diligence --data-dir /my/path # Custom data directory +python -m diligence # Alternative way to run +``` + +### Building the frontend from source + +The pip package includes a pre-built frontend. If you want to modify the UI: + +```bash +cd frontend +npm install +npm run build +cp -r dist/ ../diligence/frontend/ ``` --- -### macOS Setup +## Install — Server (Docker) -**1. Install Docker Desktop** +**Prerequisites:** Docker and Docker Compose. -Download from [docker.com/products/docker-desktop](https://www.docker.com/products/docker-desktop/). Make sure you pick the right installer for your chip: - -- **Apple Silicon** (M1, M2, M3, M4) — download the Apple Silicon .dmg -- **Intel** — download the Intel .dmg - -To check: Apple menu → About This Mac. Look for "Chip" (Apple Silicon) or "Processor" (Intel). - -Drag Docker.app to Applications and launch it. Requires macOS 13 Ventura or later. - -Alternatively, install via Homebrew: `brew install --cask docker` - -**2. Clone and run** - -Open Terminal: ```bash -git clone https://github.com/diligenceworks/diligence -cd diligence -./setup.sh -docker compose up -d +git clone https://github.com/DiligenceWorks/Diligence.git +cd Diligence +./setup.sh # generates .env with secrets +docker compose up -d # starts 4 containers +``` + +Open `http://localhost` (or your server's IP). Register your account — the first user gets admin. + +### What's running + +| Container | Role | Port | +|-----------|------|------| +| frontend | React app + nginx reverse proxy | 80 | +| backend | FastAPI application server | 8000 (internal) | +| mcp-connector | MCP SSE server for AI agents | 3001 | +| fitness-db | PostgreSQL 16 | 5432 (internal) | + +### Environment variables + +Copy `.env.example` to `.env` (or let `setup.sh` do it). Key variables: + +| Variable | Docker default | Description | +|----------|---------------|-------------| +| `SECRET_KEY` | (generated) | JWT signing key | +| `API_TOKEN` | (generated) | MCP connector auth token | +| `DATABASE_URL` | `postgresql+asyncpg://...` | Database connection | +| `BASE_URL` | `http://localhost` | Public URL for OAuth callbacks | + +--- + +## AI Agent Integration (MCP) + +Diligence exposes 14 tools via the [Model Context Protocol](https://modelcontextprotocol.io). Any MCP-compatible AI agent (Claude, custom agents) can log workouts, track food, manage meal plans, and check progress. + +### Connect your agent + +**pip install path:** +``` +URL: http://localhost:8000/mcp +Token: (shown on first run, or check ~/.diligence/.env) +``` + +**Docker path:** +``` +URL: http://localhost:3001/sse +Token: (in your .env file, API_TOKEN) +``` + +### Available tools + +| Tool | What it does | +|------|-------------| +| `get_context()` | Full profile, motivation type, programs, rules, rewards | +| `get_today()` | Daily points, gate status, activities | +| `get_week()` | Weekly summary and target progress | +| `log_activity(...)` | Log a workout, earn points | +| `log_food(...)` | Log food with macros | +| `search_food(query)` | Search Open Food Facts + USDA (400K+ foods) | +| `get_program_schedule()` | Today's scheduled workout | +| `redeem_reward(name)` | Spend points on a reward | +| `load_meal_plan(...)` | Create a meal plan | +| `get_meal_plan()` | View today's meals | +| `update_meal_compliance(...)` | Mark meals as followed/skipped | +| `get_plan_progress()` | Compliance stats | +| `configure_integration(...)` | Store encrypted credentials | +| `get_integration_status()` | Check provider connections | + +See `AGENT_GUIDE.md` for behavioral guidelines including BREQ-2 tone calibration. + +--- + +## What's inside + +### Science-based onboarding +- **PAR-Q+** physical activity readiness screening +- **TTM** (Transtheoretical Model) stage assessment +- **BREQ-2** motivation profiling — the app adapts its tone to your motivation type + +### Points engine +- Earn points for workouts, food logging, fasting, meal compliance +- Daily gate (minimum to unlock rewards) +- Weekly targets with reset +- Configurable reward shop — you decide what points are worth + +### Integrations +Strava and Polar sync are built in. The provider registry supports Garmin, Fitbit, Withings, WHOOP, and Oura (OAuth flows ready, need provider approval). USDA FoodData Central for nutrition lookup. + +--- + +## Project structure + +``` +Diligence/ + pyproject.toml # pip install config + docker-compose.yml # Docker config + diligence/ # Python package + cli.py # Entry point for pip path + main.py # FastAPI app + config.py # Settings (auto-detects SQLite vs PostgreSQL) + database.py # Dialect-agnostic database layer + models/ # 15 SQLAlchemy models + routers/ # 12 API routers + services/ # Points engine, food lookup, sync, crypto + utils/ # Auth helpers + mcp/ # MCP connector (14 tools) + frontend/ # Pre-built React app (pip path serves this) + frontend/ # React source code + backend/ # Dockerfile + requirements.txt (Docker path) + mcp-connector/ # MCP Dockerfile (Docker path) ``` --- -### Linux Setup - -Install Docker Engine and Docker Compose via your distro's package manager, or install Docker Desktop for Linux. Then: +## Development ```bash -git clone https://github.com/diligenceworks/diligence -cd diligence -./setup.sh -docker compose up -d +# Clone +git clone https://github.com/DiligenceWorks/Diligence.git +cd Diligence + +# Backend +pip install -e ".[dev]" +diligence --port 8000 + +# Frontend (separate terminal) +cd frontend +npm install +npm run dev ``` --- -Open http://localhost and create your account. First user gets admin. +## Data and privacy -### Verify +Your data never leaves your machine. There is no telemetry, no analytics, no cloud sync. The database is a single file (`~/.diligence/data.db` for pip, a Docker volume for Docker). Back it up however you back up your files. -```bash -docker compose ps -``` +Integration credentials (Strava, Polar, etc.) are encrypted at rest using Fernet with HKDF key derivation from your SECRET_KEY. -You should see 4 containers, all healthy: `frontend`, `backend`, `mcp-connector`, `fitness-db`. - -## Features - -- **Points economy** — earn from workouts, food logging, step goals. Daily gate locks rewards until you earn enough. Weekly reset. -- **Science-based onboarding** — PAR-Q+ safety screening, TTM stages of change, BREQ-2 motivation profiling. -- **AI agent integration** — 14 MCP tools for logging, coaching, meal planning, and device configuration. -- **Meal plans** — AI-generated plans with compliance tracking and points integration. -- **Activity sync** — Strava, Polar (OAuth 2.0). Garmin, Fitbit, Withings, WHOOP, Oura configurable in-app. -- **Food search** — Open Food Facts (4M+ products) + USDA FoodData Central (400K+ research-grade). -- **Program tracking** — 90-day structured programs (StrongLifts, Darebee, etc.) with day-by-day progression. -- **Configurable rewards** — you define what's worth earning. Gaming time, screen time, treats — your rules. - -## Built-in AI Coach - -Diligence includes a built-in AI coaching chat. Configure any LLM provider in **Settings → Integrations**, then open the **Coach** tab. - -Supported providers (one API key, that's it): - -| Provider | Free tier | What you get | -|----------|-----------|-------------| -| **OpenRouter** | 26 free models | 300+ models from every major provider, one key | -| **Hugging Face** | Yes | Thousands of open-source models | -| **Groq** | Yes | Ultra-fast Llama inference | -| **Ollama** | Local, free | Run any model on your own machine | -| **OpenAI** | No | GPT-4o, GPT-4o-mini | -| **Claude** | No | Claude Sonnet 4.6, Opus 4.8 | -| **Gemini** | Yes | Gemini 2.0 Flash, 2.5 Pro | -| **Custom** | — | Any OpenAI-compatible endpoint (vLLM, TGI, LiteLLM) | - -The AI coach has access to your profile, points, program schedule, and motivation type. It can log workouts, search food, and create meal plans through the chat. - -### Connecting external AI agents (MCP) - -The MCP connector at port 3001 works with any MCP-compatible agent. The built-in chat and external agents coexist — use whichever fits your workflow. - -**Claude Desktop** — add to `claude_desktop_config.json`: -```json -{ - "mcpServers": { - "diligence": { "url": "http://localhost:3001/sse" } - } -} -``` - -**Claude Code** (CLI): -```bash -claude mcp add diligence --transport sse http://localhost:3001/sse -``` - -**Cursor** — add to `.cursor/mcp.json`: -```json -{ - "mcpServers": { - "diligence": { "url": "http://localhost:3001/sse" } - } -} -``` - -**Windsurf** — add to MCP settings, same format as Cursor. - -**COROS watch owners** — add both Diligence and COROS MCP servers to your agent. The agent bridges your watch data with your fitness log: -```json -{ - "mcpServers": { - "diligence": { "url": "http://localhost:3001/sse" }, - "coros": { "url": "https://your-coros-mcp-url/sse" } - } -} -``` - -Copy the contents of [AGENT_GUIDE.md](AGENT_GUIDE.md) into your agent's system instructions for motivation-aware coaching. - -## Configuring Integrations - -All integrations are configured through the app UI (**Settings → Integrations**) or through your AI agent. No `.env` file editing or container restarts needed. - -Tell your agent: *"I want to connect my Strava"* — it will walk you through getting API credentials and store them encrypted. - -## Data Sovereignty - -Diligence is self-hosted software. Your data never leaves your server. No cloud dependency, no vendor lock-in, no telemetry. MIT license — fork it, modify it, keep it forever. - -Your fitness data — heart rate, body composition, food intake, GPS traces — is biometric data that deserves sovereignty. - -## Architecture - -``` -┌─────────────────────────────────────────────────┐ -│ nginx │ -│ (frontend, /api proxy, /mcp proxy) │ -├──────────┬─────────────────┬────────────────────┤ -│ React │ FastAPI │ MCP Connector │ -│ SPA │ Backend │ (14 tools) │ -│ │ │ port 3001 │ -│ ├──────────────────┤ │ -│ │ PostgreSQL 16 │ │ -│ │ (internal only) │ │ -└──────────┴──────────────────┴────────────────────┘ -``` - -## Stack - -- Python 3.12, FastAPI, SQLAlchemy (async) -- React 18, Vite -- PostgreSQL 16 -- FastMCP (Streamable HTTP/SSE) -- Docker Compose - -## Updating - -```bash -git pull -docker compose build -docker compose up -d -``` - -Database migrations run automatically on startup. Your data is preserved. - -## Backing Up - -Your data lives in the `fitness_db_data` Docker volume: - -```bash -docker compose exec fitness-db pg_dump -U fitness fitness_rewards > backup.sql -``` - -To restore: - -```bash -docker compose exec -i fitness-db psql -U fitness fitness_rewards < backup.sql -``` - -## Contributing - -Contributions welcome. Please open an issue to discuss before submitting a PR. +--- ## License -MIT — see [LICENSE](LICENSE). +MIT. See `LICENSE`. + +Built by [DiligenceWorks Pte. Ltd.](https://diligenceworks.online) diff --git a/backend/Dockerfile b/backend/Dockerfile index db8324a..a5025dc 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,7 +1,12 @@ FROM python:3.12-slim + WORKDIR /app + RUN apt-get update && apt-get install -y --no-install-recommends curl && rm -rf /var/lib/apt/lists/* -COPY requirements.txt . + +COPY backend/requirements.txt . RUN pip install --no-cache-dir -r requirements.txt -COPY . . -CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] + +COPY diligence/ ./diligence/ + +CMD ["uvicorn", "diligence.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/backend/app/config.py b/backend/app/config.py deleted file mode 100644 index 3c2be33..0000000 --- a/backend/app/config.py +++ /dev/null @@ -1,45 +0,0 @@ -from __future__ import annotations - -from pydantic_settings import BaseSettings -from functools import lru_cache - - -class Settings(BaseSettings): - # Database - hostname 'fitness-db' avoids collision with other 'db' containers on Coolify network - database_url: str = "postgresql+asyncpg://fitness@fitness-db:5432/fitness_rewards" - - # Auth - secret_key: str = "change-me-in-production" - crawl_enabled: bool = False # Set CRAWL_ENABLED=true to start program crawl scheduler - access_token_expire_minutes: int = 1440 # 24 hours - api_token: str = "" # MCP connector auth — generated by setup.sh - - # Strava - strava_client_id: str = "" - strava_client_secret: str = "" - - # Polar - polar_client_id: str = "" - polar_client_secret: str = "" - - # Groq (program extraction) - groq_api_key: str = "" - - # Telegram (support notifications — outbound only) - telegram_bot_token: str = "" - telegram_chat_id: str = "" - - # App - base_url: str = "http://localhost" - timezone: str = "Asia/Bangkok" - - model_config = {"env_file": ".env", "extra": "ignore"} - - -@lru_cache -def get_settings() -> Settings: - return Settings() - - -# Module-level shortcut used by services -settings = get_settings() diff --git a/backend/app/main.py b/backend/app/main.py deleted file mode 100644 index b69ab59..0000000 --- a/backend/app/main.py +++ /dev/null @@ -1,428 +0,0 @@ -from __future__ import annotations - -import asyncio -import sys -import logging -from contextlib import asynccontextmanager -from fastapi import FastAPI -from fastapi.middleware.cors import CORSMiddleware - -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger("fitness-rewards") - - -@asynccontextmanager -async def lifespan(app: FastAPI): - # Retry DB init — container may start before postgres is fully accepting connections - for attempt in range(10): - try: - from app.database import init_db - await init_db() - logger.info("Database tables created successfully") - break - except Exception as e: - logger.warning(f"DB init attempt {attempt + 1}/10 failed: {e}") - if attempt < 9: - await asyncio.sleep(3) - else: - logger.error("Could not initialize database after 10 attempts — starting without tables") - - - # SEC-05: Fail fast if SECRET_KEY not configured - from app.config import get_settings - _s = get_settings() - if _s.secret_key == "change-me-in-production": - logger.error("CRITICAL: SECRET_KEY not set. Run ./setup.sh or set SECRET_KEY in .env") - sys.exit(1) - - # Run lightweight migrations for schema changes - try: - await run_migrations() - logger.info("Migrations completed") - except Exception as e: - logger.warning(f"Migration failed (non-fatal): {e}") - - # Seed resources (non-fatal if it fails) - try: - await seed_resources() - logger.info("Resource library seeded") - except Exception as e: - logger.warning(f"Resource seeding failed (non-fatal): {e}") - - # Start background crawl queue scheduler (gated on CRAWL_ENABLED) - crawl_task = None - if _s.crawl_enabled: - try: - from app.services.crawl_scheduler import crawl_queue_loop - crawl_task = asyncio.create_task(crawl_queue_loop()) - logger.info("Crawl queue scheduler started") - except Exception as e: - logger.warning(f"Crawl scheduler failed to start (non-fatal): {e}") - else: - logger.info("Crawl scheduler disabled (set CRAWL_ENABLED=true to enable)") - - logger.info("Fitness Rewards backend started") - yield - - # Shutdown - if crawl_task: - crawl_task.cancel() - try: - await crawl_task - except asyncio.CancelledError: - pass - logger.info("Fitness Rewards backend shutting down") - - -app = FastAPI(title="Fitness Rewards", version="1.0.0", lifespan=lifespan) - -app.add_middleware( - CORSMiddleware, - allow_origins=["*"], - allow_credentials=False, # Bearer tokens don't need credentials mode - allow_methods=["*"], - allow_headers=["*"], -) - -# Import routers after app creation to avoid circular imports -from app.routers.auth import router as auth_router -from app.routers.onboarding import router as onboarding_router -from app.routers.activities import router as activities_router -from app.routers.food import router as food_router -from app.routers.points import router as points_router, rewards_router -from app.routers.integrations import router as integrations_router -from app.routers.programs import router as programs_router -from app.routers.catalog import router as catalog_router -from app.routers.support import router as support_router -from app.routers.nutrition import router as nutrition_router -from app.routers.meal_plans import router as meal_plans_router -from app.routers.ai_chat import router as ai_chat_router - -app.include_router(auth_router) -app.include_router(onboarding_router) -app.include_router(activities_router) -app.include_router(food_router) -app.include_router(points_router) -app.include_router(rewards_router) -app.include_router(integrations_router) -app.include_router(catalog_router) -app.include_router(support_router) -app.include_router(programs_router) -app.include_router(nutrition_router) -app.include_router(meal_plans_router) -app.include_router(ai_chat_router) - - -@app.get("/api/health") -async def health(): - return {"status": "ok", "version": "1.0.0"} - - - -async def run_migrations(): - """Add missing columns to existing tables (lightweight schema migration).""" - from app.database import engine - from sqlalchemy import text - - async with engine.begin() as conn: - # v1: Add equipment_list JSONB column if missing - await conn.execute(text(""" - DO $$ BEGIN - ALTER TABLE user_profiles ADD COLUMN IF NOT EXISTS equipment_list JSONB DEFAULT '[]'; - EXCEPTION WHEN undefined_table THEN NULL; - END $$; - """)) - - # v2: Add catalog columns to programs table - await conn.execute(text(""" - DO $$ BEGIN - ALTER TABLE programs ADD COLUMN IF NOT EXISTS catalog_id UUID REFERENCES program_catalog(id); - ALTER TABLE programs ADD COLUMN IF NOT EXISTS current_week INTEGER DEFAULT 1; - ALTER TABLE programs ADD COLUMN IF NOT EXISTS current_day INTEGER DEFAULT 1; - EXCEPTION WHEN undefined_table THEN NULL; - END $$; - """)) - - # v2.1: Add week_number to workout_logs for template rotation tracking - await conn.execute(text(""" - DO $$ BEGIN - ALTER TABLE workout_logs ADD COLUMN IF NOT EXISTS week_number INTEGER NOT NULL DEFAULT 1; - EXCEPTION WHEN undefined_table THEN NULL; - END $$; - """)) - - # v3: Seed keto point rules (fast_completed, keto_day, meal_logged) - # NOTE: Keto rules below are from v2. v3+ migrations (is_admin, integration_configs, - # meal plans) are added after this block. - await conn.execute(text(""" - DO $$ BEGIN - INSERT INTO point_rules (id, user_id, category, points, unit, is_active) - SELECT gen_random_uuid(), u.id, 'fast_completed', 200, 'per_event', TRUE - FROM users u - WHERE NOT EXISTS ( - SELECT 1 FROM point_rules pr - WHERE pr.user_id = u.id AND pr.category = 'fast_completed' - ); - INSERT INTO point_rules (id, user_id, category, points, unit, is_active) - SELECT gen_random_uuid(), u.id, 'keto_compliant_day', 100, 'per_event', TRUE - FROM users u - WHERE NOT EXISTS ( - SELECT 1 FROM point_rules pr - WHERE pr.user_id = u.id AND pr.category = 'keto_compliant_day' - ); - EXCEPTION WHEN undefined_table THEN NULL; - END $$; - """)) - - # v4: Add is_admin column to users - await conn.execute(text(""" - DO $$ BEGIN - ALTER TABLE users ADD COLUMN IF NOT EXISTS is_admin BOOLEAN DEFAULT FALSE; - EXCEPTION WHEN undefined_table THEN NULL; - END $$; - """)) - - # v4.1: Grant admin to first registered user if no admin exists - await conn.execute(text(""" - DO $$ BEGIN - IF NOT EXISTS (SELECT 1 FROM users WHERE is_admin = TRUE) THEN - UPDATE users SET is_admin = TRUE - WHERE id = (SELECT id FROM users ORDER BY created_at ASC LIMIT 1); - END IF; - EXCEPTION WHEN undefined_table THEN NULL; - WHEN undefined_column THEN NULL; - END $$; - """)) - - # v5: Create integration_configs table - await conn.execute(text(""" - CREATE TABLE IF NOT EXISTS integration_configs ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - user_id UUID NOT NULL REFERENCES users(id), - provider VARCHAR(50) NOT NULL, - config_key VARCHAR(100) NOT NULL, - config_value TEXT NOT NULL, - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW(), - UNIQUE(user_id, provider, config_key) - ); - """)) - - # v6: Create meal plan tables - await conn.execute(text(""" - CREATE TABLE IF NOT EXISTS meal_plans ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - user_id UUID NOT NULL REFERENCES users(id), - name VARCHAR(200) NOT NULL, - diet_type VARCHAR(50), - daily_calories INTEGER, - daily_protein_g INTEGER, - daily_carbs_g INTEGER, - daily_fat_g INTEGER, - restrictions JSONB DEFAULT '[]', - duration_days INTEGER NOT NULL, - start_date DATE NOT NULL, - status VARCHAR(20) DEFAULT 'active', - created_at TIMESTAMPTZ DEFAULT NOW() - ); - """)) - - await conn.execute(text(""" - CREATE TABLE IF NOT EXISTS meal_plan_items ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - plan_id UUID NOT NULL REFERENCES meal_plans(id) ON DELETE CASCADE, - day_number INTEGER NOT NULL, - meal_type VARCHAR(20) NOT NULL, - food_name VARCHAR(300) NOT NULL, - description TEXT, - calories INTEGER, - protein_g DECIMAL(6,1), - carbs_g DECIMAL(6,1), - fat_g DECIMAL(6,1), - fiber_g DECIMAL(6,1), - serving_size VARCHAR(100), - sort_order INTEGER DEFAULT 0, - created_at TIMESTAMPTZ DEFAULT NOW() - ); - """)) - - await conn.execute(text(""" - CREATE TABLE IF NOT EXISTS meal_compliance ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - user_id UUID NOT NULL REFERENCES users(id), - plan_id UUID NOT NULL REFERENCES meal_plans(id), - plan_item_id UUID, - compliance_date DATE NOT NULL, - status VARCHAR(20) NOT NULL, - substitution TEXT, - food_log_id UUID, - created_at TIMESTAMPTZ DEFAULT NOW() - ); - """)) - - await conn.execute(text(""" - CREATE INDEX IF NOT EXISTS idx_meal_plan_items_day ON meal_plan_items(plan_id, day_number); - """)) - await conn.execute(text(""" - CREATE INDEX IF NOT EXISTS idx_meal_compliance_date ON meal_compliance(user_id, compliance_date); - """)) - - # v7: Seed meal plan point rules - await conn.execute(text(""" - DO $$ BEGIN - INSERT INTO point_rules (id, user_id, category, points, unit, is_active) - SELECT gen_random_uuid(), u.id, 'meal_plan_followed', 40, 'per_event', TRUE - FROM users u - WHERE NOT EXISTS ( - SELECT 1 FROM point_rules pr - WHERE pr.user_id = u.id AND pr.category = 'meal_plan_followed' - ); - INSERT INTO point_rules (id, user_id, category, points, unit, is_active) - SELECT gen_random_uuid(), u.id, 'meal_plan_partial', 20, 'per_event', TRUE - FROM users u - WHERE NOT EXISTS ( - SELECT 1 FROM point_rules pr - WHERE pr.user_id = u.id AND pr.category = 'meal_plan_partial' - ); - EXCEPTION WHEN undefined_table THEN NULL; - END $$; - """)) - - -async def seed_resources(): - """Seed the resource library with curated fitness programs.""" - from app.database import async_session - from app.models.resource import Resource - from sqlalchemy import select - - async with async_session() as db: - existing = await db.execute(select(Resource).limit(1)) - if existing.scalar_one_or_none(): - return # Already seeded - - resources = [ - Resource( - name="Darebee Foundation Program", - source="darebee", - url="https://darebee.com/programs/foundation-program.html", - description="30-day beginner program. Bodyweight exercises, no equipment needed. Perfect starting point.", - goal_tags=["get_active", "feel_better"], - activity_tags=["bodyweight"], - equipment_needed="none", - ttm_stages=["precontemplation", "contemplation", "preparation"], - difficulty="beginner", - duration_days=30, - ), - Resource( - name="Darebee 30 Days of Change", - source="darebee", - url="https://darebee.com/programs/30-days-of-change.html", - description="30-day progressive program for building fitness habits. Bodyweight only.", - goal_tags=["get_active", "lose_weight", "feel_better"], - activity_tags=["bodyweight"], - equipment_needed="none", - ttm_stages=["contemplation", "preparation"], - difficulty="beginner", - duration_days=30, - ), - Resource( - name="Darebee IRONHEART", - source="darebee", - url="https://darebee.com/programs/ironheart.html", - description="Strength-focused bodyweight program. No equipment, all levels.", - goal_tags=["build_strength"], - activity_tags=["bodyweight"], - equipment_needed="none", - ttm_stages=["preparation", "action"], - difficulty="intermediate", - duration_days=30, - ), - Resource( - name="Darebee Total Body Strength", - source="darebee", - url="https://darebee.com/programs/total-body-strength.html", - description="Comprehensive strength building program using bodyweight.", - goal_tags=["build_strength"], - activity_tags=["bodyweight"], - equipment_needed="none", - ttm_stages=["action", "maintenance"], - difficulty="intermediate", - duration_days=30, - ), - Resource( - name="Darebee 8 Weeks to 5K", - source="darebee", - url="https://darebee.com/programs/8-weeks-to-5k-program.html", - description="Progressive running program from beginner to 5K in 8 weeks.", - goal_tags=["get_active", "lose_weight"], - activity_tags=["running"], - equipment_needed="none", - ttm_stages=["preparation", "action"], - difficulty="beginner", - duration_days=56, - ), - Resource( - name="StrongLifts 5x5", - source="stronglifts", - url="https://stronglifts.com/5x5/", - description="Simple barbell strength program. 3 days/week, 5 exercises, progressive overload.", - goal_tags=["build_strength"], - activity_tags=["weights"], - equipment_needed="full_gym", - ttm_stages=["preparation", "action", "maintenance"], - difficulty="beginner", - duration_days=90, - ), - Resource( - name="Darebee 30 Days of Cardio", - source="darebee", - url="https://darebee.com/programs/30-days-of-cardio.html", - description="30-day cardio program, no equipment. Great for weight loss.", - goal_tags=["lose_weight", "get_active"], - activity_tags=["bodyweight"], - equipment_needed="none", - ttm_stages=["preparation", "action"], - difficulty="beginner", - duration_days=30, - ), - Resource( - name="Darebee POWERBUILDER", - source="darebee", - url="https://darebee.com/programs/powerbuilder-program.html", - description="Advanced strength and power program using bodyweight.", - goal_tags=["build_strength"], - activity_tags=["bodyweight"], - equipment_needed="none", - ttm_stages=["action", "maintenance"], - difficulty="advanced", - duration_days=30, - ), - Resource( - name="Fitness Blender (YouTube)", - source="youtube", - url="https://www.youtube.com/@fitnessblender", - description="Free workout videos for all levels. Huge variety: HIIT, strength, yoga, pilates.", - goal_tags=["lose_weight", "build_strength", "get_active", "feel_better"], - activity_tags=["bodyweight", "yoga"], - equipment_needed="none", - ttm_stages=["contemplation", "preparation", "action", "maintenance"], - difficulty="beginner", - duration_days=None, - ), - Resource( - name="Darebee Yoga Flexibility Program", - source="darebee", - url="https://darebee.com/programs/flexibility-program.html", - description="30-day flexibility and yoga program for beginners.", - goal_tags=["feel_better"], - activity_tags=["yoga"], - equipment_needed="none", - ttm_stages=["precontemplation", "contemplation", "preparation"], - difficulty="beginner", - duration_days=30, - ), - ] - - for r in resources: - db.add(r) - await db.commit() diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py deleted file mode 100644 index 8af76b0..0000000 --- a/backend/app/models/__init__.py +++ /dev/null @@ -1,24 +0,0 @@ -from app.models.user import User -from app.models.profile import UserProfile -from app.models.program import Program -from app.models.activity import ActivityLog -from app.models.food import FoodLog -from app.models.points import PointRule, DailyTarget -from app.models.reward import Reward, RewardRedemption -from app.models.oauth import OAuthToken -from app.models.resource import Resource -from app.models.catalog import ProgramCatalog, CatalogWorkout, CrawlQueue, WorkoutLog -from app.models.support import SupportThread, SupportMessage -from app.models.nutrition import NutritionGoal, Fast, ElectrolyteLog -from app.models.integration_config import IntegrationConfig -from app.models.meal_plan import MealPlan, MealPlanItem, MealCompliance - -__all__ = [ - "User", "UserProfile", "Program", "ActivityLog", "FoodLog", - "PointRule", "DailyTarget", "Reward", "RewardRedemption", - "OAuthToken", "Resource", - "ProgramCatalog", "CatalogWorkout", "CrawlQueue", "WorkoutLog", - "SupportThread", "SupportMessage", - "NutritionGoal", "Fast", "ElectrolyteLog", - "IntegrationConfig", "MealPlan", "MealPlanItem", "MealCompliance", -] diff --git a/backend/app/utils/__init__.py b/backend/app/utils/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/backend/requirements.txt b/backend/requirements.txt index b7a7237..b16d3cc 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,12 +1,14 @@ -# Backend +# Core fastapi==0.115.6 uvicorn[standard]==0.34.0 sqlalchemy[asyncio]==2.0.36 -asyncpg==0.30.0 -alembic==1.14.0 pydantic==2.10.3 pydantic-settings==2.7.0 python-jose[cryptography]==3.3.0 bcrypt==4.2.1 httpx==0.28.1 python-multipart==0.0.18 + +# Database drivers +asyncpg==0.30.0 +aiosqlite==0.20.0 diff --git a/diligence/__init__.py b/diligence/__init__.py new file mode 100644 index 0000000..54d4733 --- /dev/null +++ b/diligence/__init__.py @@ -0,0 +1,3 @@ +"""Diligence — self-hosted fitness rewards platform with AI agent integration.""" + +__version__ = "2.0.0" diff --git a/diligence/__main__.py b/diligence/__main__.py new file mode 100644 index 0000000..16dd2f0 --- /dev/null +++ b/diligence/__main__.py @@ -0,0 +1,4 @@ +"""Allow running as: python -m diligence""" +from diligence.cli import main + +main() diff --git a/diligence/cli.py b/diligence/cli.py new file mode 100644 index 0000000..27e74e2 --- /dev/null +++ b/diligence/cli.py @@ -0,0 +1,99 @@ +"""Diligence CLI — run the fitness app from the command line.""" +from __future__ import annotations + +import argparse +import os +import sys +import secrets +import webbrowser +from pathlib import Path + + +def main(): + parser = argparse.ArgumentParser( + prog="diligence", + description="Diligence — self-hosted fitness rewards platform", + ) + parser.add_argument( + "--port", type=int, default=8000, + help="Port to run on (default: 8000)", + ) + parser.add_argument( + "--host", type=str, default="127.0.0.1", + help="Host to bind to (default: 127.0.0.1)", + ) + parser.add_argument( + "--no-browser", action="store_true", + help="Don't open the browser automatically", + ) + parser.add_argument( + "--no-mcp", action="store_true", + help="Disable the MCP connector", + ) + parser.add_argument( + "--data-dir", type=str, default=None, + help="Data directory (default: ~/.diligence)", + ) + + args = parser.parse_args() + + # Set up data directory + data_dir = Path(args.data_dir) if args.data_dir else Path.home() / ".diligence" + data_dir.mkdir(parents=True, exist_ok=True) + + # Auto-generate secrets on first run + env_file = data_dir / ".env" + if not env_file.exists(): + secret_key = secrets.token_hex(32) + api_token = secrets.token_hex(32) + env_file.write_text( + f"SECRET_KEY={secret_key}\n" + f"API_TOKEN={api_token}\n" + f"BASE_URL=http://localhost:{args.port}\n" + f"DATA_DIR={data_dir}\n" + ) + print(f"Created config at {env_file}") + print(f"MCP token: {api_token}") + + # Point pydantic-settings at the env file + os.environ.setdefault("DATA_DIR", str(data_dir)) + + # Load existing env file + if env_file.exists(): + for line in env_file.read_text().splitlines(): + line = line.strip() + if line and not line.startswith("#") and "=" in line: + key, _, value = line.partition("=") + os.environ.setdefault(key.strip(), value.strip()) + + if args.no_mcp: + os.environ["MCP_ENABLED"] = "false" + + # Open browser after a short delay + url = f"http://localhost:{args.port}" + if not args.no_browser: + import threading + + def _open(): + import time + time.sleep(2) + webbrowser.open(url) + + threading.Thread(target=_open, daemon=True).start() + + print(f"\n Diligence running at {url}") + print(f" Data: {data_dir}") + print(f" Press Ctrl+C to stop\n") + + # Run uvicorn + import uvicorn + uvicorn.run( + "diligence.main:app", + host=args.host, + port=args.port, + log_level="info", + ) + + +if __name__ == "__main__": + main() diff --git a/diligence/config.py b/diligence/config.py new file mode 100644 index 0000000..394b35c --- /dev/null +++ b/diligence/config.py @@ -0,0 +1,71 @@ +from __future__ import annotations + +from pathlib import Path +from pydantic_settings import BaseSettings +from functools import lru_cache + + +def _default_data_dir() -> Path: + """~/.diligence on all platforms.""" + d = Path.home() / ".diligence" + d.mkdir(exist_ok=True) + return d + + +class Settings(BaseSettings): + # Database — empty string triggers SQLite auto-detect (pip install path) + # Set DATABASE_URL explicitly for PostgreSQL (Docker path) + database_url: str = "" + + # Auth + secret_key: str = "change-me-in-production" + crawl_enabled: bool = False + access_token_expire_minutes: int = 1440 # 24 hours + api_token: str = "" # MCP connector auth + + # Strava + strava_client_id: str = "" + strava_client_secret: str = "" + + # Polar + polar_client_id: str = "" + polar_client_secret: str = "" + + # Groq (program extraction) + groq_api_key: str = "" + + # Telegram (support notifications) + telegram_bot_token: str = "" + telegram_chat_id: str = "" + + # App + base_url: str = "http://localhost:8000" + timezone: str = "Asia/Bangkok" + data_dir: str = str(_default_data_dir()) + + # MCP + mcp_enabled: bool = True + mcp_port: int = 3001 + + model_config = {"env_file": ".env", "extra": "ignore"} + + @property + def effective_database_url(self) -> str: + """Return the database URL, falling back to SQLite if not set.""" + if self.database_url: + return self.database_url + db_path = Path(self.data_dir) / "data.db" + return f"sqlite+aiosqlite:///{db_path}" + + @property + def is_sqlite(self) -> bool: + return self.effective_database_url.startswith("sqlite") + + +@lru_cache +def get_settings() -> Settings: + return Settings() + + +# Module-level shortcut used by services +settings = get_settings() diff --git a/backend/app/database.py b/diligence/database.py similarity index 67% rename from backend/app/database.py rename to diligence/database.py index 615156c..bc432d3 100644 --- a/backend/app/database.py +++ b/diligence/database.py @@ -2,11 +2,17 @@ from __future__ import annotations from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession from sqlalchemy.orm import DeclarativeBase -from app.config import get_settings +from diligence.config import get_settings settings = get_settings() -engine = create_async_engine(settings.database_url, echo=False, pool_size=5, max_overflow=10) +# Build engine with dialect-appropriate settings +_engine_kwargs: dict = {"echo": False} +if not settings.is_sqlite: + # PostgreSQL — connection pooling + _engine_kwargs.update(pool_size=5, max_overflow=10) + +engine = create_async_engine(settings.effective_database_url, **_engine_kwargs) async_session = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False) @@ -28,7 +34,7 @@ async def get_db(): async def init_db(): # Import all models so their tables are registered on Base.metadata - import app.models # noqa: F401 + import diligence.models # noqa: F401 async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) diff --git a/diligence/frontend/.well-known/agent-card.json b/diligence/frontend/.well-known/agent-card.json new file mode 100644 index 0000000..d9b04f7 --- /dev/null +++ b/diligence/frontend/.well-known/agent-card.json @@ -0,0 +1,102 @@ +{ + "name": "Diligence Fitness Rewards", + "description": "Self-hosted fitness rewards platform with points-based behavioral economy. Your fitness data stays on your hardware. AI agents can log activities, query progress, manage meal plans, and configure device integrations via MCP.", + "url": "{BASE_URL}/mcp", + "provider": { + "organization": "DiligenceWorks Pte. Ltd.", + "url": "https://diligenceworks.online" + }, + "version": "1.0.0", + "documentationUrl": "https://github.com/diligenceworks/diligence", + "capabilities": { + "streaming": true, + "pushNotifications": false, + "stateTransitionHistory": false + }, + "authentication": { + "schemes": ["apiKey"], + "credentials": null + }, + "defaultInputModes": ["text/plain", "application/json"], + "defaultOutputModes": ["application/json"], + "skills": [ + { + "id": "context-status", + "name": "Context & Status", + "description": "Get full user profile with motivation type (BREQ-2), active program, points status, rewards, and integration connections. Includes get_context (full brief), get_today (daily points/gate), and get_week (weekly summary).", + "tags": ["fitness", "status", "profile", "motivation"], + "examples": [ + "How am I doing today?", + "What's my weekly progress?", + "Show me my fitness dashboard" + ] + }, + { + "id": "activity-logging", + "name": "Activity Logging", + "description": "Record workouts, runs, walks, cycling, yoga, or any physical activity. Awards points based on activity type and duration. Supports structured program workouts via program_day parameter.", + "tags": ["fitness", "tracking", "points", "workout"], + "examples": [ + "I just did a 30-minute run", + "Log my StrongLifts Workout B today", + "I did 45 minutes of yoga this morning" + ] + }, + { + "id": "nutrition-tracking", + "name": "Food & Nutrition", + "description": "Log food intake with calorie and macro tracking. Search Open Food Facts (4M+ products) and USDA FoodData Central (400K+ research-grade items). Awards points for consistent food logging.", + "tags": ["nutrition", "food", "tracking", "barcode", "search"], + "examples": [ + "I had 2 eggs and toast for breakfast", + "Search for chicken breast nutrition", + "Log a salad for lunch, about 400 calories" + ] + }, + { + "id": "program-schedule", + "name": "Program Schedule", + "description": "View today's scheduled workout from the active program including exercises, sets, reps, and weight progression. Shows upcoming workouts and overall program completion percentage.", + "tags": ["program", "schedule", "workout", "planning"], + "examples": [ + "What's my workout today?", + "Show me this week's program schedule", + "How far through my program am I?" + ] + }, + { + "id": "rewards", + "name": "Rewards", + "description": "View available rewards with point costs and spend earned points. Rewards are user-configurable (gaming time, screen time, treats, etc.). Points gate must be passed before redemption.", + "tags": ["rewards", "points", "spending", "gamification"], + "examples": [ + "What rewards can I unlock?", + "Spend points on 30 minutes of gaming", + "Do I have enough points for a movie?" + ] + }, + { + "id": "meal-planning", + "name": "Meal Planning", + "description": "Create, view, and track AI-generated meal plans. The agent generates plan content; the app stores and tracks compliance. Includes load_meal_plan, get_meal_plan, update_meal_compliance, and get_plan_progress.", + "tags": ["nutrition", "meal-plan", "diet", "compliance"], + "examples": [ + "Create a 7-day keto meal plan", + "What am I supposed to eat today?", + "I followed my breakfast plan", + "How's my meal plan compliance?" + ] + }, + { + "id": "device-integration", + "name": "Device Integration", + "description": "Configure connections to fitness devices and services (Strava, Polar, Garmin, Fitbit, WHOOP, Oura, Withings). Write-only credential storage — agent can set but never read credentials back.", + "tags": ["integration", "strava", "garmin", "fitbit", "sync"], + "examples": [ + "Connect my Strava account", + "What integrations are available?", + "I want to set up my Garmin" + ] + } + ] +} diff --git a/diligence/frontend/.well-known/skills/diligence-fitness/SKILL.md b/diligence/frontend/.well-known/skills/diligence-fitness/SKILL.md new file mode 100644 index 0000000..1597825 --- /dev/null +++ b/diligence/frontend/.well-known/skills/diligence-fitness/SKILL.md @@ -0,0 +1,50 @@ +# Diligence — Agent Skill Definition + +## What Is Diligence? + +Diligence is a self-hosted fitness rewards platform. Users earn points through fitness activities (workouts, food logging, step goals) and spend them on configurable rewards (gaming time, screen time, etc.). A daily gate keeps rewards locked until enough points are earned. Points reset weekly. + +## Architecture + +- **Backend:** FastAPI (Python 3.12) + PostgreSQL 16 +- **Frontend:** React 18 + Vite, served by nginx +- **MCP Connector:** FastMCP (Streamable HTTP/SSE) on port 3001, proxied via nginx at `/mcp` +- **Deployment:** Docker Compose (4 services: frontend, backend, mcp-connector, fitness-db) + +## Connecting + +Point your MCP client to: +- **Development:** `http://localhost:3001/sse` +- **Production:** `https://your-domain/mcp` + +## Available Tools (14) + +| Tool | Category | Description | +|------|----------|-------------| +| `get_context()` | Status | Full profile, motivation, program, rules, rewards | +| `get_today(date?)` | Status | Daily points, gate status, activities | +| `get_week(start_date?)` | Status | Weekly summary, active days | +| `log_activity(category, title, duration_minutes, ...)` | Activity | Log workout, earn points | +| `log_food(meal_type, food_name, ...)` | Food | Log food with macros | +| `search_food(query)` | Food | Search Open Food Facts + USDA | +| `get_program_schedule(date?)` | Programs | Today's workout from active program | +| `redeem_reward(reward_name)` | Rewards | Spend points on a reward | +| `load_meal_plan(name, duration_days, meals, ...)` | Meal Plans | Create a full meal plan | +| `get_meal_plan(date?)` | Meal Plans | View today's planned meals | +| `update_meal_compliance(plan_item_id, status, ...)` | Meal Plans | Mark meal followed/skipped | +| `get_plan_progress(plan_id?)` | Meal Plans | Compliance statistics | +| `configure_integration(provider, credentials)` | Integrations | Store encrypted credentials (write-only) | +| `get_integration_status()` | Integrations | Check provider connection status | + +## Key Design Decisions + +- The **agent generates meal plans** — the app only stores and tracks them. Zero AI API cost. +- Integration credentials are **write-only through MCP** — agents can set but never read them back. +- The app collects **BREQ-2 motivation data** during onboarding. Use `get_context()` to read the user's motivation profile and calibrate tone accordingly. +- Points reset weekly. Each week is a fresh start. No guilt carry-over. + +## Source + +- **Repository:** https://github.com/diligenceworks/diligence +- **License:** MIT +- **Built by:** DiligenceWorks Pte. Ltd. (https://diligenceworks.online) diff --git a/diligence/frontend/assets/index-CRHpiAro.css b/diligence/frontend/assets/index-CRHpiAro.css new file mode 100644 index 0000000..4bf3276 --- /dev/null +++ b/diligence/frontend/assets/index-CRHpiAro.css @@ -0,0 +1 @@ +@import"https://fonts.googleapis.com/css2?family=Instrument+Sans:ital,wght@0,400;0,500;0,600;0,700;1,400&family=IBM+Plex+Mono:wght@400;500;600&display=swap";:root{--accent: #2952CC;--accent-dark: #1e3fa8;--accent-light: #4A78E0;--accent-bg: #edf1fc;--accent-glow: rgba(41, 82, 204, .18);--green: #0d7d5a;--green-dark: #065c40;--green-light: #30C090;--green-bg: rgba(13, 125, 90, .08);--red: #C62828;--red-bg: rgba(198, 40, 40, .06);--amber: #b07800;--amber-bg: rgba(176, 120, 0, .08);--text: #1a1f2e;--text-2: #3d4660;--text-3: #6b7490;--text-inv: #FFFFFF;--bg: #fafbfe;--bg-warm: linear-gradient(170deg, #f0f2fa 0%, #fafbfe 50%, #f5f7fd 100%);--card: #FFFFFF;--card-border: rgba(216, 220, 232, .6);--divider: rgba(216, 220, 232, .8);--shadow-1: 0 1px 3px rgba(26, 31, 46, .06);--shadow-2: 0 4px 16px rgba(26, 31, 46, .08);--shadow-3: 0 12px 40px rgba(26, 31, 46, .12);--shadow-accent: 0 4px 20px rgba(41, 82, 204, .2);--shadow-green: 0 4px 20px rgba(13, 125, 90, .2);--r: 8px;--r-sm: 6px;--r-lg: 12px;--r-full: 999px;--font: "Instrument Sans", -apple-system, sans-serif;--font-display: "Instrument Sans", -apple-system, sans-serif;--font-mono: "IBM Plex Mono", monospace}*,*:before,*:after{margin:0;padding:0;box-sizing:border-box}body{font-family:var(--font);background:var(--bg);color:var(--text);min-height:100dvh;-webkit-font-smoothing:antialiased;font-size:15px;line-height:1.5}h1,h2,h3{font-family:var(--font-display);letter-spacing:-.02em;line-height:1.2}a{color:var(--accent);text-decoration:none;font-weight:600}a:hover{opacity:.8}button{font-family:var(--font);cursor:pointer;border:none;border-radius:var(--r);padding:12px 24px;font-size:.88rem;font-weight:700;letter-spacing:.01em;transition:all .2s cubic-bezier(.4,0,.2,1);-webkit-tap-highlight-color:transparent}button:active{transform:scale(.97)}.btn-primary{background:var(--accent);color:var(--text-inv);box-shadow:var(--shadow-accent)}.btn-primary:hover{background:var(--accent-dark)}.btn-primary:disabled{opacity:.4;cursor:not-allowed;transform:none;box-shadow:none}.btn-success{background:var(--green);color:var(--text-inv);box-shadow:var(--shadow-green)}.btn-outline{background:var(--card);border:2px solid var(--divider);color:var(--text)}.btn-outline:hover{border-color:var(--accent);color:var(--accent)}.btn-ghost{background:transparent;color:var(--text-2);padding:10px 16px}.btn-ghost:hover{background:var(--accent-bg);color:var(--accent)}.btn-danger{background:var(--red);color:var(--text-inv)}.btn-sm{padding:8px 16px;font-size:.82rem;border-radius:var(--r-sm)}.btn-full{width:100%}input,select,textarea{font-family:var(--font);background:var(--card);color:var(--text);border:2px solid var(--divider);border-radius:var(--r);padding:12px 16px;font-size:.88rem;font-weight:500;width:100%;outline:none;transition:border-color .2s}input:focus,select:focus,textarea:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-glow)}input::placeholder,textarea::placeholder{color:var(--text-3);font-weight:400}.card{background:var(--card);border:1px solid var(--card-border);border-radius:var(--r-lg);padding:20px;margin-bottom:14px;box-shadow:var(--shadow-1)}.page{max-width:440px;margin:0 auto;padding:16px 16px 96px;min-height:100dvh;background:var(--bg-warm)}.page-title{font-family:var(--font-display);font-size:1.5rem;font-weight:600;margin-bottom:18px}.progress-bar{width:100%;height:10px;background:#0000000d;border-radius:var(--r-full);overflow:hidden}.progress-bar-fill{height:100%;border-radius:var(--r-full);transition:width .6s cubic-bezier(.4,0,.2,1)}.status-earned{color:var(--green)}.status-locked{color:var(--red)}.section-label{font-family:var(--font-mono);font-size:.72rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:var(--text-3);margin-bottom:10px}.checklist-item{display:flex;align-items:center;gap:12px;padding:11px 0;border-bottom:1px solid var(--divider);font-size:.9rem;font-weight:500}.checklist-item:last-child{border-bottom:none}.checklist-check{font-size:1.15rem;min-width:26px}.checklist-points{margin-left:auto;font-family:var(--font-mono);font-size:.78rem;font-weight:700;color:var(--text-3);background:#0000000a;padding:3px 10px;border-radius:var(--r-full)}.nav-bar{position:fixed;bottom:0;left:0;right:0;background:#ffffffeb;backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);border-top:1px solid var(--divider);display:flex;justify-content:space-around;padding:4px 0 env(safe-area-inset-bottom,6px);z-index:100}.nav-item{display:flex;flex-direction:column;align-items:center;gap:1px;color:var(--text-3);font-size:.62rem;font-weight:700;letter-spacing:.03em;padding:6px 14px;text-decoration:none;transition:color .15s;border-radius:var(--r-sm);position:relative}.nav-item.active{color:var(--accent)}.nav-item.active:before{content:"";position:absolute;top:-4px;left:50%;transform:translate(-50%);width:20px;height:3px;background:var(--accent);border-radius:2px}.nav-icon{font-size:1.2rem;margin-bottom:1px}.form-group{margin-bottom:16px}.form-label{display:block;font-size:.78rem;font-weight:700;color:var(--text-2);margin-bottom:6px;letter-spacing:.02em}.option-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px}.option-btn{background:var(--card);border:2px solid var(--divider);border-radius:var(--r-lg);padding:18px 12px;text-align:center;color:var(--text);font-size:.88rem;font-weight:600;cursor:pointer;transition:all .2s;box-shadow:var(--shadow-1)}.option-btn:hover{border-color:var(--accent-light);transform:translateY(-2px);box-shadow:var(--shadow-2)}.option-btn.selected{border-color:var(--accent);background:var(--accent-bg);box-shadow:0 0 0 3px var(--accent-glow)}.chip-grid{display:flex;flex-wrap:wrap;gap:8px}.chip{background:var(--card);border:2px solid var(--divider);border-radius:var(--r-full);padding:8px 18px;font-size:.84rem;font-weight:600;cursor:pointer;transition:all .2s}.chip:hover{border-color:var(--accent-light)}.chip.selected{border-color:var(--accent);background:var(--accent);color:var(--text-inv)}.reward-card{display:flex;align-items:center;justify-content:space-between;padding:14px 0;border-bottom:1px solid var(--divider)}.reward-card:last-child{border-bottom:none}.likert-row{display:flex;align-items:center;gap:10px;margin:8px 0 18px;justify-content:center}.likert-btn{width:44px;height:44px;border-radius:50%;background:var(--card);border:2px solid var(--divider);color:var(--text);font-weight:600;font-size:.88rem;display:flex;align-items:center;justify-content:center;cursor:pointer;transition:all .2s;box-shadow:var(--shadow-1)}.likert-btn:hover{border-color:var(--accent);transform:scale(1.1)}.likert-btn.selected{border-color:transparent;background:var(--accent);color:var(--text-inv);box-shadow:var(--shadow-accent);transform:scale(1.1)}.likert-labels{display:flex;justify-content:space-between;font-size:.68rem;color:var(--text-3);font-weight:700;text-transform:uppercase;letter-spacing:.06em}.gate-banner{text-align:center;padding:28px 20px;border-radius:var(--r-lg);margin-bottom:14px;position:relative}.gate-earned{background:linear-gradient(135deg,#e8f5e9,#c8e6c9,#e0f2f1);border:2px solid rgba(0,200,83,.2)}.gate-locked{background:linear-gradient(135deg,#fff3e0,#ffe0b2,#fff8e1);border:2px solid rgba(255,87,34,.15)}.gate-pts{font-family:var(--font-mono);font-size:2.8rem;font-weight:700;letter-spacing:-.04em;line-height:1;margin:8px 0}.meal-section{margin-bottom:18px}.meal-title{font-size:.72rem;font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:var(--text-3);margin-bottom:6px;padding-bottom:6px;border-bottom:2px solid var(--divider)}.loading{display:flex;justify-content:center;padding:60px;color:var(--text-3);font-weight:600}.error-msg{background:var(--red-ghost);border:2px solid rgba(255,23,68,.15);padding:12px 16px;border-radius:var(--r);color:var(--red);font-weight:600;font-size:.88rem;margin-bottom:14px}@keyframes fadeUp{0%{opacity:0;transform:translateY(16px)}to{opacity:1;transform:translateY(0)}}@keyframes popIn{0%{opacity:0;transform:scale(.92)}to{opacity:1;transform:scale(1)}}@keyframes countUp{0%{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}.page>.card:nth-child(1){animation:fadeUp .3s ease-out both}.page>.card:nth-child(2){animation:fadeUp .3s ease-out .06s both}.page>.card:nth-child(3){animation:fadeUp .3s ease-out .12s both}.page>.card:nth-child(4){animation:fadeUp .3s ease-out .18s both}.gate-banner{animation:popIn .35s cubic-bezier(.4,0,.2,1) both}.gate-pts{animation:countUp .4s ease-out .15s both}@media(prefers-color-scheme:dark){:root{--accent: #6B9BFF;--accent-dark: #4A78E0;--accent-light: #8BB5FF;--accent-bg: #101630;--accent-glow: rgba(107, 155, 255, .18);--green: #30C090;--green-dark: #20A070;--green-light: #50D8A8;--green-bg: rgba(48, 192, 144, .1);--red: #EF5350;--red-bg: rgba(239, 83, 80, .1);--amber: #F0B040;--amber-bg: rgba(240, 176, 64, .1);--text: #eaecf4;--text-2: #b0b8d0;--text-3: #7882a0;--text-inv: #0a0b12;--bg: #0a0b12;--bg-warm: linear-gradient(170deg, #0e1020 0%, #0a0b12 50%, #0c0e1a 100%);--card: #12131d;--card-border: rgba(34, 40, 64, .8);--divider: rgba(34, 40, 64, .8);--shadow-1: 0 1px 3px rgba(0, 0, 0, .3);--shadow-2: 0 4px 16px rgba(0, 0, 0, .4);--shadow-3: 0 12px 40px rgba(0, 0, 0, .5);--shadow-accent: 0 4px 20px rgba(107, 155, 255, .15);--shadow-green: 0 4px 20px rgba(48, 192, 144, .15)}body{background:var(--bg);color:var(--text)}.nav-bar{background:#0a0b12eb}input,select,textarea{background:var(--bg);color:var(--text)}.gate-earned{background:linear-gradient(135deg,#30c0901f,#30c0900f);border-color:#30c09040}.gate-locked{background:linear-gradient(135deg,#6b9bff1a,#6b9bff0d);border-color:#6b9bff26}.option-btn,.chip,.likert-btn{background:var(--card)}}:root[data-theme=dark]{--accent: #6B9BFF;--accent-dark: #4A78E0;--accent-light: #8BB5FF;--accent-bg: #101630;--accent-glow: rgba(107, 155, 255, .18);--green: #30C090;--green-dark: #20A070;--green-light: #50D8A8;--green-bg: rgba(48, 192, 144, .1);--red: #EF5350;--red-bg: rgba(239, 83, 80, .1);--amber: #F0B040;--amber-bg: rgba(240, 176, 64, .1);--text: #eaecf4;--text-2: #b0b8d0;--text-3: #7882a0;--text-inv: #0a0b12;--bg: #0a0b12;--bg-warm: linear-gradient(170deg, #0e1020 0%, #0a0b12 50%, #0c0e1a 100%);--card: #12131d;--card-border: rgba(34, 40, 64, .8);--divider: rgba(34, 40, 64, .8);--shadow-1: 0 1px 3px rgba(0, 0, 0, .3);--shadow-2: 0 4px 16px rgba(0, 0, 0, .4);--shadow-3: 0 12px 40px rgba(0, 0, 0, .5);--shadow-accent: 0 4px 20px rgba(107, 155, 255, .15);--shadow-green: 0 4px 20px rgba(48, 192, 144, .15)}::-webkit-scrollbar{width:4px}::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb{background:var(--divider);border-radius:4px}@media(max-width:380px){.page{padding:12px 10px 96px}.gate-pts{font-size:2.2rem}.option-grid{gap:8px}.option-btn{padding:14px 10px}} diff --git a/diligence/frontend/assets/index-DIoo6DtB.js b/diligence/frontend/assets/index-DIoo6DtB.js new file mode 100644 index 0000000..4e5afaa --- /dev/null +++ b/diligence/frontend/assets/index-DIoo6DtB.js @@ -0,0 +1,68 @@ +function Jf(o,c){for(var s=0;sd[p]})}}}return Object.freeze(Object.defineProperty(o,Symbol.toStringTag,{value:"Module"}))}(function(){const c=document.createElement("link").relList;if(c&&c.supports&&c.supports("modulepreload"))return;for(const p of document.querySelectorAll('link[rel="modulepreload"]'))d(p);new MutationObserver(p=>{for(const y of p)if(y.type==="childList")for(const g of y.addedNodes)g.tagName==="LINK"&&g.rel==="modulepreload"&&d(g)}).observe(document,{childList:!0,subtree:!0});function s(p){const y={};return p.integrity&&(y.integrity=p.integrity),p.referrerPolicy&&(y.referrerPolicy=p.referrerPolicy),p.crossOrigin==="use-credentials"?y.credentials="include":p.crossOrigin==="anonymous"?y.credentials="omit":y.credentials="same-origin",y}function d(p){if(p.ep)return;p.ep=!0;const y=s(p);fetch(p.href,y)}})();function Wc(o){return o&&o.__esModule&&Object.prototype.hasOwnProperty.call(o,"default")?o.default:o}var Zo={exports:{}},Lr={},ea={exports:{}},ce={};/** + * @license React + * react.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var sc;function Yf(){if(sc)return ce;sc=1;var o=Symbol.for("react.element"),c=Symbol.for("react.portal"),s=Symbol.for("react.fragment"),d=Symbol.for("react.strict_mode"),p=Symbol.for("react.profiler"),y=Symbol.for("react.provider"),g=Symbol.for("react.context"),P=Symbol.for("react.forward_ref"),S=Symbol.for("react.suspense"),T=Symbol.for("react.memo"),z=Symbol.for("react.lazy"),v=Symbol.iterator;function L(w){return w===null||typeof w!="object"?null:(w=v&&w[v]||w["@@iterator"],typeof w=="function"?w:null)}var A={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},R=Object.assign,N={};function j(w,O,ae){this.props=w,this.context=O,this.refs=N,this.updater=ae||A}j.prototype.isReactComponent={},j.prototype.setState=function(w,O){if(typeof w!="object"&&typeof w!="function"&&w!=null)throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,w,O,"setState")},j.prototype.forceUpdate=function(w){this.updater.enqueueForceUpdate(this,w,"forceUpdate")};function F(){}F.prototype=j.prototype;function _(w,O,ae){this.props=w,this.context=O,this.refs=N,this.updater=ae||A}var W=_.prototype=new F;W.constructor=_,R(W,j.prototype),W.isPureReactComponent=!0;var B=Array.isArray,Z=Object.prototype.hasOwnProperty,fe={current:null},we={key:!0,ref:!0,__self:!0,__source:!0};function xe(w,O,ae){var ue,se={},b=null,ee=null;if(O!=null)for(ue in O.ref!==void 0&&(ee=O.ref),O.key!==void 0&&(b=""+O.key),O)Z.call(O,ue)&&!we.hasOwnProperty(ue)&&(se[ue]=O[ue]);var ie=arguments.length-2;if(ie===1)se.children=ae;else if(1>>1,O=H[w];if(0>>1;wp(se,V))bp(ee,se)?(H[w]=ee,H[b]=V,w=b):(H[w]=se,H[ue]=V,w=ue);else if(bp(ee,V))H[w]=ee,H[b]=V,w=b;else break e}}return I}function p(H,I){var V=H.sortIndex-I.sortIndex;return V!==0?V:H.id-I.id}if(typeof performance=="object"&&typeof performance.now=="function"){var y=performance;o.unstable_now=function(){return y.now()}}else{var g=Date,P=g.now();o.unstable_now=function(){return g.now()-P}}var S=[],T=[],z=1,v=null,L=3,A=!1,R=!1,N=!1,j=typeof setTimeout=="function"?setTimeout:null,F=typeof clearTimeout=="function"?clearTimeout:null,_=typeof setImmediate<"u"?setImmediate:null;typeof navigator<"u"&&navigator.scheduling!==void 0&&navigator.scheduling.isInputPending!==void 0&&navigator.scheduling.isInputPending.bind(navigator.scheduling);function W(H){for(var I=s(T);I!==null;){if(I.callback===null)d(T);else if(I.startTime<=H)d(T),I.sortIndex=I.expirationTime,c(S,I);else break;I=s(T)}}function B(H){if(N=!1,W(H),!R)if(s(S)!==null)R=!0,We(Z);else{var I=s(T);I!==null&&Se(B,I.startTime-H)}}function Z(H,I){R=!1,N&&(N=!1,F(xe),xe=-1),A=!0;var V=L;try{for(W(I),v=s(S);v!==null&&(!(v.expirationTime>I)||H&&!pe());){var w=v.callback;if(typeof w=="function"){v.callback=null,L=v.priorityLevel;var O=w(v.expirationTime<=I);I=o.unstable_now(),typeof O=="function"?v.callback=O:v===s(S)&&d(S),W(I)}else d(S);v=s(S)}if(v!==null)var ae=!0;else{var ue=s(T);ue!==null&&Se(B,ue.startTime-I),ae=!1}return ae}finally{v=null,L=V,A=!1}}var fe=!1,we=null,xe=-1,re=5,oe=-1;function pe(){return!(o.unstable_now()-oeH||125w?(H.sortIndex=V,c(T,H),s(S)===null&&H===s(T)&&(N?(F(xe),xe=-1):N=!0,Se(B,V-w))):(H.sortIndex=O,c(S,H),R||A||(R=!0,We(Z))),H},o.unstable_shouldYield=pe,o.unstable_wrapCallback=function(H){var I=L;return function(){var V=L;L=I;try{return H.apply(this,arguments)}finally{L=V}}}})(ra)),ra}var pc;function ep(){return pc||(pc=1,na.exports=Zf()),na.exports}/** + * @license React + * react-dom.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var mc;function tp(){if(mc)return lt;mc=1;var o=ua(),c=ep();function s(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;n"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),S=Object.prototype.hasOwnProperty,T=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,z={},v={};function L(e){return S.call(v,e)?!0:S.call(z,e)?!1:T.test(e)?v[e]=!0:(z[e]=!0,!1)}function A(e,t,n,r){if(n!==null&&n.type===0)return!1;switch(typeof t){case"function":case"symbol":return!0;case"boolean":return r?!1:n!==null?!n.acceptsBooleans:(e=e.toLowerCase().slice(0,5),e!=="data-"&&e!=="aria-");default:return!1}}function R(e,t,n,r){if(t===null||typeof t>"u"||A(e,t,n,r))return!0;if(r)return!1;if(n!==null)switch(n.type){case 3:return!t;case 4:return t===!1;case 5:return isNaN(t);case 6:return isNaN(t)||1>t}return!1}function N(e,t,n,r,i,a,u){this.acceptsBooleans=t===2||t===3||t===4,this.attributeName=r,this.attributeNamespace=i,this.mustUseProperty=n,this.propertyName=e,this.type=t,this.sanitizeURL=a,this.removeEmptyString=u}var j={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(e){j[e]=new N(e,0,!1,e,null,!1,!1)}),[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(e){var t=e[0];j[t]=new N(t,1,!1,e[1],null,!1,!1)}),["contentEditable","draggable","spellCheck","value"].forEach(function(e){j[e]=new N(e,2,!1,e.toLowerCase(),null,!1,!1)}),["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(e){j[e]=new N(e,2,!1,e,null,!1,!1)}),"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(e){j[e]=new N(e,3,!1,e.toLowerCase(),null,!1,!1)}),["checked","multiple","muted","selected"].forEach(function(e){j[e]=new N(e,3,!0,e,null,!1,!1)}),["capture","download"].forEach(function(e){j[e]=new N(e,4,!1,e,null,!1,!1)}),["cols","rows","size","span"].forEach(function(e){j[e]=new N(e,6,!1,e,null,!1,!1)}),["rowSpan","start"].forEach(function(e){j[e]=new N(e,5,!1,e.toLowerCase(),null,!1,!1)});var F=/[\-:]([a-z])/g;function _(e){return e[1].toUpperCase()}"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(e){var t=e.replace(F,_);j[t]=new N(t,1,!1,e,null,!1,!1)}),"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(e){var t=e.replace(F,_);j[t]=new N(t,1,!1,e,"http://www.w3.org/1999/xlink",!1,!1)}),["xml:base","xml:lang","xml:space"].forEach(function(e){var t=e.replace(F,_);j[t]=new N(t,1,!1,e,"http://www.w3.org/XML/1998/namespace",!1,!1)}),["tabIndex","crossOrigin"].forEach(function(e){j[e]=new N(e,1,!1,e.toLowerCase(),null,!1,!1)}),j.xlinkHref=new N("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1),["src","href","action","formAction"].forEach(function(e){j[e]=new N(e,1,!1,e.toLowerCase(),null,!0,!0)});function W(e,t,n,r){var i=j.hasOwnProperty(t)?j[t]:null;(i!==null?i.type!==0:r||!(2f||i[u]!==a[f]){var h=` +`+i[u].replace(" at new "," at ");return e.displayName&&h.includes("")&&(h=h.replace("",e.displayName)),h}while(1<=u&&0<=f);break}}}finally{ae=!1,Error.prepareStackTrace=n}return(e=e?e.displayName||e.name:"")?O(e):""}function se(e){switch(e.tag){case 5:return O(e.type);case 16:return O("Lazy");case 13:return O("Suspense");case 19:return O("SuspenseList");case 0:case 2:case 15:return e=ue(e.type,!1),e;case 11:return e=ue(e.type.render,!1),e;case 1:return e=ue(e.type,!0),e;default:return""}}function b(e){if(e==null)return null;if(typeof e=="function")return e.displayName||e.name||null;if(typeof e=="string")return e;switch(e){case we:return"Fragment";case fe:return"Portal";case re:return"Profiler";case xe:return"StrictMode";case ve:return"Suspense";case Te:return"SuspenseList"}if(typeof e=="object")switch(e.$$typeof){case pe:return(e.displayName||"Context")+".Consumer";case oe:return(e._context.displayName||"Context")+".Provider";case Ne:var t=e.render;return e=e.displayName,e||(e=t.displayName||t.name||"",e=e!==""?"ForwardRef("+e+")":"ForwardRef"),e;case Me:return t=e.displayName||null,t!==null?t:b(e.type)||"Memo";case We:t=e._payload,e=e._init;try{return b(e(t))}catch{}}return null}function ee(e){var t=e.type;switch(e.tag){case 24:return"Cache";case 9:return(t.displayName||"Context")+".Consumer";case 10:return(t._context.displayName||"Context")+".Provider";case 18:return"DehydratedFragment";case 11:return e=t.render,e=e.displayName||e.name||"",t.displayName||(e!==""?"ForwardRef("+e+")":"ForwardRef");case 7:return"Fragment";case 5:return t;case 4:return"Portal";case 3:return"Root";case 6:return"Text";case 16:return b(t);case 8:return t===xe?"StrictMode":"Mode";case 22:return"Offscreen";case 12:return"Profiler";case 21:return"Scope";case 13:return"Suspense";case 19:return"SuspenseList";case 25:return"TracingMarker";case 1:case 0:case 17:case 2:case 14:case 15:if(typeof t=="function")return t.displayName||t.name||null;if(typeof t=="string")return t}return null}function ie(e){switch(typeof e){case"boolean":case"number":case"string":case"undefined":return e;case"object":return e;default:return""}}function de(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(t==="checkbox"||t==="radio")}function ye(e){var t=de(e)?"checked":"value",n=Object.getOwnPropertyDescriptor(e.constructor.prototype,t),r=""+e[t];if(!e.hasOwnProperty(t)&&typeof n<"u"&&typeof n.get=="function"&&typeof n.set=="function"){var i=n.get,a=n.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return i.call(this)},set:function(u){r=""+u,a.call(this,u)}}),Object.defineProperty(e,t,{enumerable:n.enumerable}),{getValue:function(){return r},setValue:function(u){r=""+u},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}function Or(e){e._valueTracker||(e._valueTracker=ye(e))}function ma(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var n=t.getValue(),r="";return e&&(r=de(e)?e.checked?"true":"false":e.value),e=r,e!==n?(t.setValue(e),!0):!1}function Mr(e){if(e=e||(typeof document<"u"?document:void 0),typeof e>"u")return null;try{return e.activeElement||e.body}catch{return e.body}}function ii(e,t){var n=t.checked;return V({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:n??e._wrapperState.initialChecked})}function ha(e,t){var n=t.defaultValue==null?"":t.defaultValue,r=t.checked!=null?t.checked:t.defaultChecked;n=ie(t.value!=null?t.value:n),e._wrapperState={initialChecked:r,initialValue:n,controlled:t.type==="checkbox"||t.type==="radio"?t.checked!=null:t.value!=null}}function ga(e,t){t=t.checked,t!=null&&W(e,"checked",t,!1)}function oi(e,t){ga(e,t);var n=ie(t.value),r=t.type;if(n!=null)r==="number"?(n===0&&e.value===""||e.value!=n)&&(e.value=""+n):e.value!==""+n&&(e.value=""+n);else if(r==="submit"||r==="reset"){e.removeAttribute("value");return}t.hasOwnProperty("value")?ai(e,t.type,n):t.hasOwnProperty("defaultValue")&&ai(e,t.type,ie(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}function va(e,t,n){if(t.hasOwnProperty("value")||t.hasOwnProperty("defaultValue")){var r=t.type;if(!(r!=="submit"&&r!=="reset"||t.value!==void 0&&t.value!==null))return;t=""+e._wrapperState.initialValue,n||t===e.value||(e.value=t),e.defaultValue=t}n=e.name,n!==""&&(e.name=""),e.defaultChecked=!!e._wrapperState.initialChecked,n!==""&&(e.name=n)}function ai(e,t,n){(t!=="number"||Mr(e.ownerDocument)!==e)&&(n==null?e.defaultValue=""+e._wrapperState.initialValue:e.defaultValue!==""+n&&(e.defaultValue=""+n))}var Jn=Array.isArray;function Sn(e,t,n,r){if(e=e.options,t){t={};for(var i=0;i"+t.valueOf().toString()+"",t=Dr.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;t.firstChild;)e.appendChild(t.firstChild)}});function Yn(e,t){if(t){var n=e.firstChild;if(n&&n===e.lastChild&&n.nodeType===3){n.nodeValue=t;return}}e.textContent=t}var Gn={animationIterationCount:!0,aspectRatio:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},Xc=["Webkit","ms","Moz","O"];Object.keys(Gn).forEach(function(e){Xc.forEach(function(t){t=t+e.charAt(0).toUpperCase()+e.substring(1),Gn[t]=Gn[e]})});function ka(e,t,n){return t==null||typeof t=="boolean"||t===""?"":n||typeof t!="number"||t===0||Gn.hasOwnProperty(e)&&Gn[e]?(""+t).trim():t+"px"}function _a(e,t){e=e.style;for(var n in t)if(t.hasOwnProperty(n)){var r=n.indexOf("--")===0,i=ka(n,t[n],r);n==="float"&&(n="cssFloat"),r?e.setProperty(n,i):e[n]=i}}var qc=V({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0});function ci(e,t){if(t){if(qc[e]&&(t.children!=null||t.dangerouslySetInnerHTML!=null))throw Error(s(137,e));if(t.dangerouslySetInnerHTML!=null){if(t.children!=null)throw Error(s(60));if(typeof t.dangerouslySetInnerHTML!="object"||!("__html"in t.dangerouslySetInnerHTML))throw Error(s(61))}if(t.style!=null&&typeof t.style!="object")throw Error(s(62))}}function di(e,t){if(e.indexOf("-")===-1)return typeof t.is=="string";switch(e){case"annotation-xml":case"color-profile":case"font-face":case"font-face-src":case"font-face-uri":case"font-face-format":case"font-face-name":case"missing-glyph":return!1;default:return!0}}var fi=null;function pi(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}var mi=null,wn=null,jn=null;function Ca(e){if(e=yr(e)){if(typeof mi!="function")throw Error(s(280));var t=e.stateNode;t&&(t=ul(t),mi(e.stateNode,e.type,t))}}function Na(e){wn?jn?jn.push(e):jn=[e]:wn=e}function Ea(){if(wn){var e=wn,t=jn;if(jn=wn=null,Ca(e),t)for(e=0;e>>=0,e===0?32:31-(ud(e)/cd|0)|0}var Hr=64,Qr=4194304;function er(e){switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e&4194240;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return e&130023424;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return e}}function Kr(e,t){var n=e.pendingLanes;if(n===0)return 0;var r=0,i=e.suspendedLanes,a=e.pingedLanes,u=n&268435455;if(u!==0){var f=u&~i;f!==0?r=er(f):(a&=u,a!==0&&(r=er(a)))}else u=n&~i,u!==0?r=er(u):a!==0&&(r=er(a));if(r===0)return 0;if(t!==0&&t!==r&&(t&i)===0&&(i=r&-r,a=t&-t,i>=a||i===16&&(a&4194240)!==0))return t;if((r&4)!==0&&(r|=n&16),t=e.entangledLanes,t!==0)for(e=e.entanglements,t&=r;0n;n++)t.push(e);return t}function tr(e,t,n){e.pendingLanes|=t,t!==536870912&&(e.suspendedLanes=0,e.pingedLanes=0),e=e.eventTimes,t=31-ht(t),e[t]=n}function md(e,t){var n=e.pendingLanes&~t;e.pendingLanes=t,e.suspendedLanes=0,e.pingedLanes=0,e.expiredLanes&=t,e.mutableReadLanes&=t,e.entangledLanes&=t,t=e.entanglements;var r=e.eventTimes;for(e=e.expirationTimes;0=ur),ts=" ",ns=!1;function rs(e,t){switch(e){case"keyup":return $d.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function ls(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}var Cn=!1;function Vd(e,t){switch(e){case"compositionend":return ls(t);case"keypress":return t.which!==32?null:(ns=!0,ts);case"textInput":return e=t.data,e===ts&&ns?null:e;default:return null}}function Hd(e,t){if(Cn)return e==="compositionend"||!Ri&&rs(e,t)?(e=Ya(),qr=Ni=$t=null,Cn=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=r}e:{for(;n;){if(n.nextSibling){n=n.nextSibling;break e}n=n.parentNode}n=void 0}n=ds(n)}}function ps(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?ps(e,t.parentNode):"contains"in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function ms(){for(var e=window,t=Mr();t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href=="string"}catch{n=!1}if(n)e=t.contentWindow;else break;t=Mr(e.document)}return t}function Fi(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t==="input"&&(e.type==="text"||e.type==="search"||e.type==="tel"||e.type==="url"||e.type==="password")||t==="textarea"||e.contentEditable==="true")}function ef(e){var t=ms(),n=e.focusedElem,r=e.selectionRange;if(t!==n&&n&&n.ownerDocument&&ps(n.ownerDocument.documentElement,n)){if(r!==null&&Fi(n)){if(t=r.start,e=r.end,e===void 0&&(e=t),"selectionStart"in n)n.selectionStart=t,n.selectionEnd=Math.min(e,n.value.length);else if(e=(t=n.ownerDocument||document)&&t.defaultView||window,e.getSelection){e=e.getSelection();var i=n.textContent.length,a=Math.min(r.start,i);r=r.end===void 0?a:Math.min(r.end,i),!e.extend&&a>r&&(i=r,r=a,a=i),i=fs(n,a);var u=fs(n,r);i&&u&&(e.rangeCount!==1||e.anchorNode!==i.node||e.anchorOffset!==i.offset||e.focusNode!==u.node||e.focusOffset!==u.offset)&&(t=t.createRange(),t.setStart(i.node,i.offset),e.removeAllRanges(),a>r?(e.addRange(t),e.extend(u.node,u.offset)):(t.setEnd(u.node,u.offset),e.addRange(t)))}}for(t=[],e=n;e=e.parentNode;)e.nodeType===1&&t.push({element:e,left:e.scrollLeft,top:e.scrollTop});for(typeof n.focus=="function"&&n.focus(),n=0;n=document.documentMode,Nn=null,Wi=null,pr=null,Bi=!1;function hs(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;Bi||Nn==null||Nn!==Mr(r)||(r=Nn,"selectionStart"in r&&Fi(r)?r={start:r.selectionStart,end:r.selectionEnd}:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection(),r={anchorNode:r.anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset}),pr&&fr(pr,r)||(pr=r,r=ol(Wi,"onSelect"),0bn||(e.current=Yi[bn],Yi[bn]=null,bn--)}function je(e,t){bn++,Yi[bn]=e.current,e.current=t}var Qt={},Qe=Ht(Qt),Ze=Ht(!1),sn=Qt;function Rn(e,t){var n=e.type.contextTypes;if(!n)return Qt;var r=e.stateNode;if(r&&r.__reactInternalMemoizedUnmaskedChildContext===t)return r.__reactInternalMemoizedMaskedChildContext;var i={},a;for(a in n)i[a]=t[a];return r&&(e=e.stateNode,e.__reactInternalMemoizedUnmaskedChildContext=t,e.__reactInternalMemoizedMaskedChildContext=i),i}function et(e){return e=e.childContextTypes,e!=null}function cl(){_e(Ze),_e(Qe)}function Ts(e,t,n){if(Qe.current!==Qt)throw Error(s(168));je(Qe,t),je(Ze,n)}function bs(e,t,n){var r=e.stateNode;if(t=t.childContextTypes,typeof r.getChildContext!="function")return n;r=r.getChildContext();for(var i in r)if(!(i in t))throw Error(s(108,ee(e)||"Unknown",i));return V({},n,r)}function dl(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||Qt,sn=Qe.current,je(Qe,e),je(Ze,Ze.current),!0}function Rs(e,t,n){var r=e.stateNode;if(!r)throw Error(s(169));n?(e=bs(e,t,sn),r.__reactInternalMemoizedMergedChildContext=e,_e(Ze),_e(Qe),je(Qe,e)):_e(Ze),je(Ze,n)}var Pt=null,fl=!1,Gi=!1;function Ls(e){Pt===null?Pt=[e]:Pt.push(e)}function pf(e){fl=!0,Ls(e)}function Kt(){if(!Gi&&Pt!==null){Gi=!0;var e=0,t=ge;try{var n=Pt;for(ge=1;e>=u,i-=u,zt=1<<32-ht(t)+i|n<le?($e=ne,ne=null):$e=ne.sibling;var he=M(k,ne,C[le],U);if(he===null){ne===null&&(ne=$e);break}e&&ne&&he.alternate===null&&t(k,ne),x=a(he,x,le),te===null?q=he:te.sibling=he,te=he,ne=$e}if(le===C.length)return n(k,ne),Ce&&cn(k,le),q;if(ne===null){for(;lele?($e=ne,ne=null):$e=ne.sibling;var nn=M(k,ne,he.value,U);if(nn===null){ne===null&&(ne=$e);break}e&&ne&&nn.alternate===null&&t(k,ne),x=a(nn,x,le),te===null?q=nn:te.sibling=nn,te=nn,ne=$e}if(he.done)return n(k,ne),Ce&&cn(k,le),q;if(ne===null){for(;!he.done;le++,he=C.next())he=$(k,he.value,U),he!==null&&(x=a(he,x,le),te===null?q=he:te.sibling=he,te=he);return Ce&&cn(k,le),q}for(ne=r(k,ne);!he.done;le++,he=C.next())he=Q(ne,k,le,he.value,U),he!==null&&(e&&he.alternate!==null&&ne.delete(he.key===null?le:he.key),x=a(he,x,le),te===null?q=he:te.sibling=he,te=he);return e&&ne.forEach(function(Kf){return t(k,Kf)}),Ce&&cn(k,le),q}function Le(k,x,C,U){if(typeof C=="object"&&C!==null&&C.type===we&&C.key===null&&(C=C.props.children),typeof C=="object"&&C!==null){switch(C.$$typeof){case Z:e:{for(var q=C.key,te=x;te!==null;){if(te.key===q){if(q=C.type,q===we){if(te.tag===7){n(k,te.sibling),x=i(te,C.props.children),x.return=k,k=x;break e}}else if(te.elementType===q||typeof q=="object"&&q!==null&&q.$$typeof===We&&Ms(q)===te.type){n(k,te.sibling),x=i(te,C.props),x.ref=xr(k,te,C),x.return=k,k=x;break e}n(k,te);break}else t(k,te);te=te.sibling}C.type===we?(x=yn(C.props.children,k.mode,U,C.key),x.return=k,k=x):(U=Dl(C.type,C.key,C.props,null,k.mode,U),U.ref=xr(k,x,C),U.return=k,k=U)}return u(k);case fe:e:{for(te=C.key;x!==null;){if(x.key===te)if(x.tag===4&&x.stateNode.containerInfo===C.containerInfo&&x.stateNode.implementation===C.implementation){n(k,x.sibling),x=i(x,C.children||[]),x.return=k,k=x;break e}else{n(k,x);break}else t(k,x);x=x.sibling}x=Jo(C,k.mode,U),x.return=k,k=x}return u(k);case We:return te=C._init,Le(k,x,te(C._payload),U)}if(Jn(C))return G(k,x,C,U);if(I(C))return X(k,x,C,U);gl(k,C)}return typeof C=="string"&&C!==""||typeof C=="number"?(C=""+C,x!==null&&x.tag===6?(n(k,x.sibling),x=i(x,C),x.return=k,k=x):(n(k,x),x=Ko(C,k.mode,U),x.return=k,k=x),u(k)):n(k,x)}return Le}var Wn=Ds(!0),As=Ds(!1),vl=Ht(null),yl=null,Bn=null,no=null;function ro(){no=Bn=yl=null}function lo(e){var t=vl.current;_e(vl),e._currentValue=t}function io(e,t,n){for(;e!==null;){var r=e.alternate;if((e.childLanes&t)!==t?(e.childLanes|=t,r!==null&&(r.childLanes|=t)):r!==null&&(r.childLanes&t)!==t&&(r.childLanes|=t),e===n)break;e=e.return}}function On(e,t){yl=e,no=Bn=null,e=e.dependencies,e!==null&&e.firstContext!==null&&((e.lanes&t)!==0&&(tt=!0),e.firstContext=null)}function dt(e){var t=e._currentValue;if(no!==e)if(e={context:e,memoizedValue:t,next:null},Bn===null){if(yl===null)throw Error(s(308));Bn=e,yl.dependencies={lanes:0,firstContext:e}}else Bn=Bn.next=e;return t}var dn=null;function oo(e){dn===null?dn=[e]:dn.push(e)}function $s(e,t,n,r){var i=t.interleaved;return i===null?(n.next=n,oo(t)):(n.next=i.next,i.next=n),t.interleaved=n,bt(e,r)}function bt(e,t){e.lanes|=t;var n=e.alternate;for(n!==null&&(n.lanes|=t),n=e,e=e.return;e!==null;)e.childLanes|=t,n=e.alternate,n!==null&&(n.childLanes|=t),n=e,e=e.return;return n.tag===3?n.stateNode:null}var Jt=!1;function ao(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,interleaved:null,lanes:0},effects:null}}function Us(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,effects:e.effects})}function Rt(e,t){return{eventTime:e,lane:t,tag:0,payload:null,callback:null,next:null}}function Yt(e,t,n){var r=e.updateQueue;if(r===null)return null;if(r=r.shared,(me&2)!==0){var i=r.pending;return i===null?t.next=t:(t.next=i.next,i.next=t),r.pending=t,bt(e,n)}return i=r.interleaved,i===null?(t.next=t,oo(r)):(t.next=i.next,i.next=t),r.interleaved=t,bt(e,n)}function xl(e,t,n){if(t=t.updateQueue,t!==null&&(t=t.shared,(n&4194240)!==0)){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,wi(e,n)}}function Vs(e,t){var n=e.updateQueue,r=e.alternate;if(r!==null&&(r=r.updateQueue,n===r)){var i=null,a=null;if(n=n.firstBaseUpdate,n!==null){do{var u={eventTime:n.eventTime,lane:n.lane,tag:n.tag,payload:n.payload,callback:n.callback,next:null};a===null?i=a=u:a=a.next=u,n=n.next}while(n!==null);a===null?i=a=t:a=a.next=t}else i=a=t;n={baseState:r.baseState,firstBaseUpdate:i,lastBaseUpdate:a,shared:r.shared,effects:r.effects},e.updateQueue=n;return}e=n.lastBaseUpdate,e===null?n.firstBaseUpdate=t:e.next=t,n.lastBaseUpdate=t}function Sl(e,t,n,r){var i=e.updateQueue;Jt=!1;var a=i.firstBaseUpdate,u=i.lastBaseUpdate,f=i.shared.pending;if(f!==null){i.shared.pending=null;var h=f,E=h.next;h.next=null,u===null?a=E:u.next=E,u=h;var D=e.alternate;D!==null&&(D=D.updateQueue,f=D.lastBaseUpdate,f!==u&&(f===null?D.firstBaseUpdate=E:f.next=E,D.lastBaseUpdate=h))}if(a!==null){var $=i.baseState;u=0,D=E=h=null,f=a;do{var M=f.lane,Q=f.eventTime;if((r&M)===M){D!==null&&(D=D.next={eventTime:Q,lane:0,tag:f.tag,payload:f.payload,callback:f.callback,next:null});e:{var G=e,X=f;switch(M=t,Q=n,X.tag){case 1:if(G=X.payload,typeof G=="function"){$=G.call(Q,$,M);break e}$=G;break e;case 3:G.flags=G.flags&-65537|128;case 0:if(G=X.payload,M=typeof G=="function"?G.call(Q,$,M):G,M==null)break e;$=V({},$,M);break e;case 2:Jt=!0}}f.callback!==null&&f.lane!==0&&(e.flags|=64,M=i.effects,M===null?i.effects=[f]:M.push(f))}else Q={eventTime:Q,lane:M,tag:f.tag,payload:f.payload,callback:f.callback,next:null},D===null?(E=D=Q,h=$):D=D.next=Q,u|=M;if(f=f.next,f===null){if(f=i.shared.pending,f===null)break;M=f,f=M.next,M.next=null,i.lastBaseUpdate=M,i.shared.pending=null}}while(!0);if(D===null&&(h=$),i.baseState=h,i.firstBaseUpdate=E,i.lastBaseUpdate=D,t=i.shared.interleaved,t!==null){i=t;do u|=i.lane,i=i.next;while(i!==t)}else a===null&&(i.shared.lanes=0);mn|=u,e.lanes=u,e.memoizedState=$}}function Hs(e,t,n){if(e=t.effects,t.effects=null,e!==null)for(t=0;tn?n:4,e(!0);var r=po.transition;po.transition={};try{e(!1),t()}finally{ge=n,po.transition=r}}function cu(){return ft().memoizedState}function vf(e,t,n){var r=Zt(e);if(n={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null},du(e))fu(t,n);else if(n=$s(e,t,n,r),n!==null){var i=qe();wt(n,e,r,i),pu(n,t,r)}}function yf(e,t,n){var r=Zt(e),i={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null};if(du(e))fu(t,i);else{var a=e.alternate;if(e.lanes===0&&(a===null||a.lanes===0)&&(a=t.lastRenderedReducer,a!==null))try{var u=t.lastRenderedState,f=a(u,n);if(i.hasEagerState=!0,i.eagerState=f,gt(f,u)){var h=t.interleaved;h===null?(i.next=i,oo(t)):(i.next=h.next,h.next=i),t.interleaved=i;return}}catch{}finally{}n=$s(e,t,i,r),n!==null&&(i=qe(),wt(n,e,r,i),pu(n,t,r))}}function du(e){var t=e.alternate;return e===Pe||t!==null&&t===Pe}function fu(e,t){kr=kl=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function pu(e,t,n){if((n&4194240)!==0){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,wi(e,n)}}var Nl={readContext:dt,useCallback:Ke,useContext:Ke,useEffect:Ke,useImperativeHandle:Ke,useInsertionEffect:Ke,useLayoutEffect:Ke,useMemo:Ke,useReducer:Ke,useRef:Ke,useState:Ke,useDebugValue:Ke,useDeferredValue:Ke,useTransition:Ke,useMutableSource:Ke,useSyncExternalStore:Ke,useId:Ke,unstable_isNewReconciler:!1},xf={readContext:dt,useCallback:function(e,t){return Ct().memoizedState=[e,t===void 0?null:t],e},useContext:dt,useEffect:nu,useImperativeHandle:function(e,t,n){return n=n!=null?n.concat([e]):null,_l(4194308,4,iu.bind(null,t,e),n)},useLayoutEffect:function(e,t){return _l(4194308,4,e,t)},useInsertionEffect:function(e,t){return _l(4,2,e,t)},useMemo:function(e,t){var n=Ct();return t=t===void 0?null:t,e=e(),n.memoizedState=[e,t],e},useReducer:function(e,t,n){var r=Ct();return t=n!==void 0?n(t):t,r.memoizedState=r.baseState=t,e={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:t},r.queue=e,e=e.dispatch=vf.bind(null,Pe,e),[r.memoizedState,e]},useRef:function(e){var t=Ct();return e={current:e},t.memoizedState=e},useState:eu,useDebugValue:So,useDeferredValue:function(e){return Ct().memoizedState=e},useTransition:function(){var e=eu(!1),t=e[0];return e=gf.bind(null,e[1]),Ct().memoizedState=e,[t,e]},useMutableSource:function(){},useSyncExternalStore:function(e,t,n){var r=Pe,i=Ct();if(Ce){if(n===void 0)throw Error(s(407));n=n()}else{if(n=t(),Ae===null)throw Error(s(349));(pn&30)!==0||Ys(r,t,n)}i.memoizedState=n;var a={value:n,getSnapshot:t};return i.queue=a,nu(Xs.bind(null,r,a,e),[e]),r.flags|=2048,Nr(9,Gs.bind(null,r,a,n,t),void 0,null),n},useId:function(){var e=Ct(),t=Ae.identifierPrefix;if(Ce){var n=Tt,r=zt;n=(r&~(1<<32-ht(r)-1)).toString(32)+n,t=":"+t+"R"+n,n=_r++,0<\/script>",e=e.removeChild(e.firstChild)):typeof r.is=="string"?e=u.createElement(n,{is:r.is}):(e=u.createElement(n),n==="select"&&(u=e,r.multiple?u.multiple=!0:r.size&&(u.size=r.size))):e=u.createElementNS(e,n),e[kt]=t,e[vr]=r,Lu(e,t,!1,!1),t.stateNode=e;e:{switch(u=di(n,r),n){case"dialog":ke("cancel",e),ke("close",e),i=r;break;case"iframe":case"object":case"embed":ke("load",e),i=r;break;case"video":case"audio":for(i=0;iUn&&(t.flags|=128,r=!0,Er(a,!1),t.lanes=4194304)}else{if(!r)if(e=wl(u),e!==null){if(t.flags|=128,r=!0,n=e.updateQueue,n!==null&&(t.updateQueue=n,t.flags|=4),Er(a,!0),a.tail===null&&a.tailMode==="hidden"&&!u.alternate&&!Ce)return Je(t),null}else 2*Re()-a.renderingStartTime>Un&&n!==1073741824&&(t.flags|=128,r=!0,Er(a,!1),t.lanes=4194304);a.isBackwards?(u.sibling=t.child,t.child=u):(n=a.last,n!==null?n.sibling=u:t.child=u,a.last=u)}return a.tail!==null?(t=a.tail,a.rendering=t,a.tail=t.sibling,a.renderingStartTime=Re(),t.sibling=null,n=Ee.current,je(Ee,r?n&1|2:n&1),t):(Je(t),null);case 22:case 23:return Vo(),r=t.memoizedState!==null,e!==null&&e.memoizedState!==null!==r&&(t.flags|=8192),r&&(t.mode&1)!==0?(st&1073741824)!==0&&(Je(t),t.subtreeFlags&6&&(t.flags|=8192)):Je(t),null;case 24:return null;case 25:return null}throw Error(s(156,t.tag))}function Ef(e,t){switch(qi(t),t.tag){case 1:return et(t.type)&&cl(),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return Mn(),_e(Ze),_e(Qe),fo(),e=t.flags,(e&65536)!==0&&(e&128)===0?(t.flags=e&-65537|128,t):null;case 5:return uo(t),null;case 13:if(_e(Ee),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(s(340));Fn()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return _e(Ee),null;case 4:return Mn(),null;case 10:return lo(t.type._context),null;case 22:case 23:return Vo(),null;case 24:return null;default:return null}}var Tl=!1,Ye=!1,Pf=typeof WeakSet=="function"?WeakSet:Set,Y=null;function An(e,t){var n=e.ref;if(n!==null)if(typeof n=="function")try{n(null)}catch(r){be(e,t,r)}else n.current=null}function Ro(e,t,n){try{n()}catch(r){be(e,t,r)}}var Wu=!1;function zf(e,t){if(Ui=Gr,e=ms(),Fi(e)){if("selectionStart"in e)var n={start:e.selectionStart,end:e.selectionEnd};else e:{n=(n=e.ownerDocument)&&n.defaultView||window;var r=n.getSelection&&n.getSelection();if(r&&r.rangeCount!==0){n=r.anchorNode;var i=r.anchorOffset,a=r.focusNode;r=r.focusOffset;try{n.nodeType,a.nodeType}catch{n=null;break e}var u=0,f=-1,h=-1,E=0,D=0,$=e,M=null;t:for(;;){for(var Q;$!==n||i!==0&&$.nodeType!==3||(f=u+i),$!==a||r!==0&&$.nodeType!==3||(h=u+r),$.nodeType===3&&(u+=$.nodeValue.length),(Q=$.firstChild)!==null;)M=$,$=Q;for(;;){if($===e)break t;if(M===n&&++E===i&&(f=u),M===a&&++D===r&&(h=u),(Q=$.nextSibling)!==null)break;$=M,M=$.parentNode}$=Q}n=f===-1||h===-1?null:{start:f,end:h}}else n=null}n=n||{start:0,end:0}}else n=null;for(Vi={focusedElem:e,selectionRange:n},Gr=!1,Y=t;Y!==null;)if(t=Y,e=t.child,(t.subtreeFlags&1028)!==0&&e!==null)e.return=t,Y=e;else for(;Y!==null;){t=Y;try{var G=t.alternate;if((t.flags&1024)!==0)switch(t.tag){case 0:case 11:case 15:break;case 1:if(G!==null){var X=G.memoizedProps,Le=G.memoizedState,k=t.stateNode,x=k.getSnapshotBeforeUpdate(t.elementType===t.type?X:yt(t.type,X),Le);k.__reactInternalSnapshotBeforeUpdate=x}break;case 3:var C=t.stateNode.containerInfo;C.nodeType===1?C.textContent="":C.nodeType===9&&C.documentElement&&C.removeChild(C.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(s(163))}}catch(U){be(t,t.return,U)}if(e=t.sibling,e!==null){e.return=t.return,Y=e;break}Y=t.return}return G=Wu,Wu=!1,G}function Pr(e,t,n){var r=t.updateQueue;if(r=r!==null?r.lastEffect:null,r!==null){var i=r=r.next;do{if((i.tag&e)===e){var a=i.destroy;i.destroy=void 0,a!==void 0&&Ro(t,n,a)}i=i.next}while(i!==r)}}function bl(e,t){if(t=t.updateQueue,t=t!==null?t.lastEffect:null,t!==null){var n=t=t.next;do{if((n.tag&e)===e){var r=n.create;n.destroy=r()}n=n.next}while(n!==t)}}function Lo(e){var t=e.ref;if(t!==null){var n=e.stateNode;switch(e.tag){case 5:e=n;break;default:e=n}typeof t=="function"?t(e):t.current=e}}function Bu(e){var t=e.alternate;t!==null&&(e.alternate=null,Bu(t)),e.child=null,e.deletions=null,e.sibling=null,e.tag===5&&(t=e.stateNode,t!==null&&(delete t[kt],delete t[vr],delete t[Ji],delete t[df],delete t[ff])),e.stateNode=null,e.return=null,e.dependencies=null,e.memoizedProps=null,e.memoizedState=null,e.pendingProps=null,e.stateNode=null,e.updateQueue=null}function Ou(e){return e.tag===5||e.tag===3||e.tag===4}function Mu(e){e:for(;;){for(;e.sibling===null;){if(e.return===null||Ou(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.flags&2||e.child===null||e.tag===4)continue e;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function Io(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.nodeType===8?n.parentNode.insertBefore(e,t):n.insertBefore(e,t):(n.nodeType===8?(t=n.parentNode,t.insertBefore(e,n)):(t=n,t.appendChild(e)),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=sl));else if(r!==4&&(e=e.child,e!==null))for(Io(e,t,n),e=e.sibling;e!==null;)Io(e,t,n),e=e.sibling}function Fo(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(r!==4&&(e=e.child,e!==null))for(Fo(e,t,n),e=e.sibling;e!==null;)Fo(e,t,n),e=e.sibling}var Ue=null,xt=!1;function Gt(e,t,n){for(n=n.child;n!==null;)Du(e,t,n),n=n.sibling}function Du(e,t,n){if(jt&&typeof jt.onCommitFiberUnmount=="function")try{jt.onCommitFiberUnmount(Vr,n)}catch{}switch(n.tag){case 5:Ye||An(n,t);case 6:var r=Ue,i=xt;Ue=null,Gt(e,t,n),Ue=r,xt=i,Ue!==null&&(xt?(e=Ue,n=n.stateNode,e.nodeType===8?e.parentNode.removeChild(n):e.removeChild(n)):Ue.removeChild(n.stateNode));break;case 18:Ue!==null&&(xt?(e=Ue,n=n.stateNode,e.nodeType===8?Ki(e.parentNode,n):e.nodeType===1&&Ki(e,n),or(e)):Ki(Ue,n.stateNode));break;case 4:r=Ue,i=xt,Ue=n.stateNode.containerInfo,xt=!0,Gt(e,t,n),Ue=r,xt=i;break;case 0:case 11:case 14:case 15:if(!Ye&&(r=n.updateQueue,r!==null&&(r=r.lastEffect,r!==null))){i=r=r.next;do{var a=i,u=a.destroy;a=a.tag,u!==void 0&&((a&2)!==0||(a&4)!==0)&&Ro(n,t,u),i=i.next}while(i!==r)}Gt(e,t,n);break;case 1:if(!Ye&&(An(n,t),r=n.stateNode,typeof r.componentWillUnmount=="function"))try{r.props=n.memoizedProps,r.state=n.memoizedState,r.componentWillUnmount()}catch(f){be(n,t,f)}Gt(e,t,n);break;case 21:Gt(e,t,n);break;case 22:n.mode&1?(Ye=(r=Ye)||n.memoizedState!==null,Gt(e,t,n),Ye=r):Gt(e,t,n);break;default:Gt(e,t,n)}}function Au(e){var t=e.updateQueue;if(t!==null){e.updateQueue=null;var n=e.stateNode;n===null&&(n=e.stateNode=new Pf),t.forEach(function(r){var i=Of.bind(null,e,r);n.has(r)||(n.add(r),r.then(i,i))})}}function St(e,t){var n=t.deletions;if(n!==null)for(var r=0;ri&&(i=u),r&=~a}if(r=i,r=Re()-r,r=(120>r?120:480>r?480:1080>r?1080:1920>r?1920:3e3>r?3e3:4320>r?4320:1960*bf(r/1960))-r,10e?16:e,qt===null)var r=!1;else{if(e=qt,qt=null,Wl=0,(me&6)!==0)throw Error(s(331));var i=me;for(me|=4,Y=e.current;Y!==null;){var a=Y,u=a.child;if((Y.flags&16)!==0){var f=a.deletions;if(f!==null){for(var h=0;hRe()-Oo?gn(e,0):Bo|=n),rt(e,t)}function ec(e,t){t===0&&((e.mode&1)===0?t=1:(t=Qr,Qr<<=1,(Qr&130023424)===0&&(Qr=4194304)));var n=qe();e=bt(e,t),e!==null&&(tr(e,t,n),rt(e,n))}function Bf(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),ec(e,n)}function Of(e,t){var n=0;switch(e.tag){case 13:var r=e.stateNode,i=e.memoizedState;i!==null&&(n=i.retryLane);break;case 19:r=e.stateNode;break;default:throw Error(s(314))}r!==null&&r.delete(t),ec(e,n)}var tc;tc=function(e,t,n){if(e!==null)if(e.memoizedProps!==t.pendingProps||Ze.current)tt=!0;else{if((e.lanes&n)===0&&(t.flags&128)===0)return tt=!1,Cf(e,t,n);tt=(e.flags&131072)!==0}else tt=!1,Ce&&(t.flags&1048576)!==0&&Is(t,ml,t.index);switch(t.lanes=0,t.tag){case 2:var r=t.type;zl(e,t),e=t.pendingProps;var i=Rn(t,Qe.current);On(t,n),i=ho(null,t,r,e,i,n);var a=go();return t.flags|=1,typeof i=="object"&&i!==null&&typeof i.render=="function"&&i.$$typeof===void 0?(t.tag=1,t.memoizedState=null,t.updateQueue=null,et(r)?(a=!0,dl(t)):a=!1,t.memoizedState=i.state!==null&&i.state!==void 0?i.state:null,ao(t),i.updater=El,t.stateNode=i,i._reactInternals=t,jo(t,r,e,n),t=No(null,t,r,!0,a,n)):(t.tag=0,Ce&&a&&Xi(t),Xe(null,t,i,n),t=t.child),t;case 16:r=t.elementType;e:{switch(zl(e,t),e=t.pendingProps,i=r._init,r=i(r._payload),t.type=r,i=t.tag=Df(r),e=yt(r,e),i){case 0:t=Co(null,t,r,e,n);break e;case 1:t=Eu(null,t,r,e,n);break e;case 11:t=ju(null,t,r,e,n);break e;case 14:t=ku(null,t,r,yt(r.type,e),n);break e}throw Error(s(306,r,""))}return t;case 0:return r=t.type,i=t.pendingProps,i=t.elementType===r?i:yt(r,i),Co(e,t,r,i,n);case 1:return r=t.type,i=t.pendingProps,i=t.elementType===r?i:yt(r,i),Eu(e,t,r,i,n);case 3:e:{if(Pu(t),e===null)throw Error(s(387));r=t.pendingProps,a=t.memoizedState,i=a.element,Us(e,t),Sl(t,r,null,n);var u=t.memoizedState;if(r=u.element,a.isDehydrated)if(a={element:r,isDehydrated:!1,cache:u.cache,pendingSuspenseBoundaries:u.pendingSuspenseBoundaries,transitions:u.transitions},t.updateQueue.baseState=a,t.memoizedState=a,t.flags&256){i=Dn(Error(s(423)),t),t=zu(e,t,r,n,i);break e}else if(r!==i){i=Dn(Error(s(424)),t),t=zu(e,t,r,n,i);break e}else for(at=Vt(t.stateNode.containerInfo.firstChild),ot=t,Ce=!0,vt=null,n=As(t,null,r,n),t.child=n;n;)n.flags=n.flags&-3|4096,n=n.sibling;else{if(Fn(),r===i){t=Lt(e,t,n);break e}Xe(e,t,r,n)}t=t.child}return t;case 5:return Qs(t),e===null&&eo(t),r=t.type,i=t.pendingProps,a=e!==null?e.memoizedProps:null,u=i.children,Hi(r,i)?u=null:a!==null&&Hi(r,a)&&(t.flags|=32),Nu(e,t),Xe(e,t,u,n),t.child;case 6:return e===null&&eo(t),null;case 13:return Tu(e,t,n);case 4:return so(t,t.stateNode.containerInfo),r=t.pendingProps,e===null?t.child=Wn(t,null,r,n):Xe(e,t,r,n),t.child;case 11:return r=t.type,i=t.pendingProps,i=t.elementType===r?i:yt(r,i),ju(e,t,r,i,n);case 7:return Xe(e,t,t.pendingProps,n),t.child;case 8:return Xe(e,t,t.pendingProps.children,n),t.child;case 12:return Xe(e,t,t.pendingProps.children,n),t.child;case 10:e:{if(r=t.type._context,i=t.pendingProps,a=t.memoizedProps,u=i.value,je(vl,r._currentValue),r._currentValue=u,a!==null)if(gt(a.value,u)){if(a.children===i.children&&!Ze.current){t=Lt(e,t,n);break e}}else for(a=t.child,a!==null&&(a.return=t);a!==null;){var f=a.dependencies;if(f!==null){u=a.child;for(var h=f.firstContext;h!==null;){if(h.context===r){if(a.tag===1){h=Rt(-1,n&-n),h.tag=2;var E=a.updateQueue;if(E!==null){E=E.shared;var D=E.pending;D===null?h.next=h:(h.next=D.next,D.next=h),E.pending=h}}a.lanes|=n,h=a.alternate,h!==null&&(h.lanes|=n),io(a.return,n,t),f.lanes|=n;break}h=h.next}}else if(a.tag===10)u=a.type===t.type?null:a.child;else if(a.tag===18){if(u=a.return,u===null)throw Error(s(341));u.lanes|=n,f=u.alternate,f!==null&&(f.lanes|=n),io(u,n,t),u=a.sibling}else u=a.child;if(u!==null)u.return=a;else for(u=a;u!==null;){if(u===t){u=null;break}if(a=u.sibling,a!==null){a.return=u.return,u=a;break}u=u.return}a=u}Xe(e,t,i.children,n),t=t.child}return t;case 9:return i=t.type,r=t.pendingProps.children,On(t,n),i=dt(i),r=r(i),t.flags|=1,Xe(e,t,r,n),t.child;case 14:return r=t.type,i=yt(r,t.pendingProps),i=yt(r.type,i),ku(e,t,r,i,n);case 15:return _u(e,t,t.type,t.pendingProps,n);case 17:return r=t.type,i=t.pendingProps,i=t.elementType===r?i:yt(r,i),zl(e,t),t.tag=1,et(r)?(e=!0,dl(t)):e=!1,On(t,n),hu(t,r,i),jo(t,r,i,n),No(null,t,r,!0,e,n);case 19:return Ru(e,t,n);case 22:return Cu(e,t,n)}throw Error(s(156,t.tag))};function nc(e,t){return Fa(e,t)}function Mf(e,t,n,r){this.tag=e,this.key=n,this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null,this.index=0,this.ref=null,this.pendingProps=t,this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null,this.mode=r,this.subtreeFlags=this.flags=0,this.deletions=null,this.childLanes=this.lanes=0,this.alternate=null}function mt(e,t,n,r){return new Mf(e,t,n,r)}function Qo(e){return e=e.prototype,!(!e||!e.isReactComponent)}function Df(e){if(typeof e=="function")return Qo(e)?1:0;if(e!=null){if(e=e.$$typeof,e===Ne)return 11;if(e===Me)return 14}return 2}function tn(e,t){var n=e.alternate;return n===null?(n=mt(e.tag,t,e.key,e.mode),n.elementType=e.elementType,n.type=e.type,n.stateNode=e.stateNode,n.alternate=e,e.alternate=n):(n.pendingProps=t,n.type=e.type,n.flags=0,n.subtreeFlags=0,n.deletions=null),n.flags=e.flags&14680064,n.childLanes=e.childLanes,n.lanes=e.lanes,n.child=e.child,n.memoizedProps=e.memoizedProps,n.memoizedState=e.memoizedState,n.updateQueue=e.updateQueue,t=e.dependencies,n.dependencies=t===null?null:{lanes:t.lanes,firstContext:t.firstContext},n.sibling=e.sibling,n.index=e.index,n.ref=e.ref,n}function Dl(e,t,n,r,i,a){var u=2;if(r=e,typeof e=="function")Qo(e)&&(u=1);else if(typeof e=="string")u=5;else e:switch(e){case we:return yn(n.children,i,a,t);case xe:u=8,i|=8;break;case re:return e=mt(12,n,t,i|2),e.elementType=re,e.lanes=a,e;case ve:return e=mt(13,n,t,i),e.elementType=ve,e.lanes=a,e;case Te:return e=mt(19,n,t,i),e.elementType=Te,e.lanes=a,e;case Se:return Al(n,i,a,t);default:if(typeof e=="object"&&e!==null)switch(e.$$typeof){case oe:u=10;break e;case pe:u=9;break e;case Ne:u=11;break e;case Me:u=14;break e;case We:u=16,r=null;break e}throw Error(s(130,e==null?e:typeof e,""))}return t=mt(u,n,t,i),t.elementType=e,t.type=r,t.lanes=a,t}function yn(e,t,n,r){return e=mt(7,e,r,t),e.lanes=n,e}function Al(e,t,n,r){return e=mt(22,e,r,t),e.elementType=Se,e.lanes=n,e.stateNode={isHidden:!1},e}function Ko(e,t,n){return e=mt(6,e,null,t),e.lanes=n,e}function Jo(e,t,n){return t=mt(4,e.children!==null?e.children:[],e.key,t),t.lanes=n,t.stateNode={containerInfo:e.containerInfo,pendingChildren:null,implementation:e.implementation},t}function Af(e,t,n,r,i){this.tag=t,this.containerInfo=e,this.finishedWork=this.pingCache=this.current=this.pendingChildren=null,this.timeoutHandle=-1,this.callbackNode=this.pendingContext=this.context=null,this.callbackPriority=0,this.eventTimes=Si(0),this.expirationTimes=Si(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=Si(0),this.identifierPrefix=r,this.onRecoverableError=i,this.mutableSourceEagerHydrationData=null}function Yo(e,t,n,r,i,a,u,f,h){return e=new Af(e,t,n,f,h),t===1?(t=1,a===!0&&(t|=8)):t=0,a=mt(3,null,null,t),e.current=a,a.stateNode=e,a.memoizedState={element:r,isDehydrated:n,cache:null,transitions:null,pendingSuspenseBoundaries:null},ao(a),e}function $f(e,t,n){var r=3"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(o)}catch(c){console.error(c)}}return o(),ta.exports=tp(),ta.exports}var gc;function np(){if(gc)return Jl;gc=1;var o=Oc();return Jl.createRoot=o.createRoot,Jl.hydrateRoot=o.hydrateRoot,Jl}var rp=np();const lp=Wc(rp);Oc();/** + * @remix-run/router v1.23.3 + * + * Copyright (c) Remix Software Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE.md file in the root directory of this source tree. + * + * @license MIT + */function Wr(){return Wr=Object.assign?Object.assign.bind():function(o){for(var c=1;c"u")throw new Error(c)}function ca(o,c){if(!o){typeof console<"u"&&console.warn(c);try{throw new Error(c)}catch{}}}function op(){return Math.random().toString(36).substr(2,8)}function yc(o,c){return{usr:o.state,key:o.key,idx:c}}function ia(o,c,s,d){return s===void 0&&(s=null),Wr({pathname:typeof o=="string"?o:o.pathname,search:"",hash:""},typeof c=="string"?Qn(c):c,{state:s,key:c&&c.key||d||op()})}function Zl(o){let{pathname:c="/",search:s="",hash:d=""}=o;return s&&s!=="?"&&(c+=s.charAt(0)==="?"?s:"?"+s),d&&d!=="#"&&(c+=d.charAt(0)==="#"?d:"#"+d),c}function Qn(o){let c={};if(o){let s=o.indexOf("#");s>=0&&(c.hash=o.substr(s),o=o.substr(0,s));let d=o.indexOf("?");d>=0&&(c.search=o.substr(d),o=o.substr(0,d)),o&&(c.pathname=o)}return c}function ap(o,c,s,d){d===void 0&&(d={});let{window:p=document.defaultView,v5Compat:y=!1}=d,g=p.history,P=rn.Pop,S=null,T=z();T==null&&(T=0,g.replaceState(Wr({},g.state,{idx:T}),""));function z(){return(g.state||{idx:null}).idx}function v(){P=rn.Pop;let j=z(),F=j==null?null:j-T;T=j,S&&S({action:P,location:N.location,delta:F})}function L(j,F){P=rn.Push;let _=ia(N.location,j,F);T=z()+1;let W=yc(_,T),B=N.createHref(_);try{g.pushState(W,"",B)}catch(Z){if(Z instanceof DOMException&&Z.name==="DataCloneError")throw Z;p.location.assign(B)}y&&S&&S({action:P,location:N.location,delta:1})}function A(j,F){P=rn.Replace;let _=ia(N.location,j,F);T=z();let W=yc(_,T),B=N.createHref(_);g.replaceState(W,"",B),y&&S&&S({action:P,location:N.location,delta:0})}function R(j){let F=p.location.origin!=="null"?p.location.origin:p.location.href,_=typeof j=="string"?j:Zl(j);return _=_.replace(/ $/,"%20"),ze(F,"No window.location.(origin|href) available to create URL for href: "+_),new URL(_,F)}let N={get action(){return P},get location(){return o(p,g)},listen(j){if(S)throw new Error("A history only accepts one active listener");return p.addEventListener(vc,v),S=j,()=>{p.removeEventListener(vc,v),S=null}},createHref(j){return c(p,j)},createURL:R,encodeLocation(j){let F=R(j);return{pathname:F.pathname,search:F.search,hash:F.hash}},push:L,replace:A,go(j){return g.go(j)}};return N}var xc;(function(o){o.data="data",o.deferred="deferred",o.redirect="redirect",o.error="error"})(xc||(xc={}));function sp(o,c,s){return s===void 0&&(s="/"),up(o,c,s)}function up(o,c,s,d){let p=typeof c=="string"?Qn(c):c,y=Hn(p.pathname||"/",s);if(y==null)return null;let g=Mc(o);cp(g);let P=null,S=wp(y);for(let T=0;P==null&&T{let S={relativePath:P===void 0?y.path||"":P,caseSensitive:y.caseSensitive===!0,childrenIndex:g,route:y};S.relativePath.startsWith("/")&&(ze(S.relativePath.startsWith(d),'Absolute route path "'+S.relativePath+'" nested under path '+('"'+d+'" is not valid. An absolute child route path ')+"must start with the combined path of all its parent routes."),S.relativePath=S.relativePath.slice(d.length));let T=ln([d,S.relativePath]),z=s.concat(S);y.children&&y.children.length>0&&(ze(y.index!==!0,"Index routes must not have child routes. Please remove "+('all child routes from route path "'+T+'".')),Mc(y.children,c,z,T)),!(y.path==null&&!y.index)&&c.push({path:T,score:vp(T,y.index),routesMeta:z})};return o.forEach((y,g)=>{var P;if(y.path===""||!((P=y.path)!=null&&P.includes("?")))p(y,g);else for(let S of Dc(y.path))p(y,g,S)}),c}function Dc(o){let c=o.split("/");if(c.length===0)return[];let[s,...d]=c,p=s.endsWith("?"),y=s.replace(/\?$/,"");if(d.length===0)return p?[y,""]:[y];let g=Dc(d.join("/")),P=[];return P.push(...g.map(S=>S===""?y:[y,S].join("/"))),p&&P.push(...g),P.map(S=>o.startsWith("/")&&S===""?"/":S)}function cp(o){o.sort((c,s)=>c.score!==s.score?s.score-c.score:yp(c.routesMeta.map(d=>d.childrenIndex),s.routesMeta.map(d=>d.childrenIndex)))}const dp=/^:[\w-]+$/,fp=3,pp=2,mp=1,hp=10,gp=-2,Sc=o=>o==="*";function vp(o,c){let s=o.split("/"),d=s.length;return s.some(Sc)&&(d+=gp),c&&(d+=pp),s.filter(p=>!Sc(p)).reduce((p,y)=>p+(dp.test(y)?fp:y===""?mp:hp),d)}function yp(o,c){return o.length===c.length&&o.slice(0,-1).every((d,p)=>d===c[p])?o[o.length-1]-c[c.length-1]:0}function xp(o,c,s){let{routesMeta:d}=o,p={},y="/",g=[];for(let P=0;P{let{paramName:L,isOptional:A}=z;if(L==="*"){let N=P[v]||"";g=y.slice(0,y.length-N.length).replace(/(.)\/+$/,"$1")}const R=P[v];return A&&!R?T[L]=void 0:T[L]=(R||"").replace(/%2F/g,"/"),T},{}),pathname:y,pathnameBase:g,pattern:o}}function Sp(o,c,s){c===void 0&&(c=!1),s===void 0&&(s=!0),ca(o==="*"||!o.endsWith("*")||o.endsWith("/*"),'Route path "'+o+'" will be treated as if it were '+('"'+o.replace(/\*$/,"/*")+'" because the `*` character must ')+"always follow a `/` in the pattern. To get rid of this warning, "+('please change the route path to "'+o.replace(/\*$/,"/*")+'".'));let d=[],p="^"+o.replace(/\/*\*?$/,"").replace(/^\/*/,"/").replace(/[\\.*+^${}|()[\]]/g,"\\$&").replace(/\/:([\w-]+)(\?)?/g,(g,P,S)=>(d.push({paramName:P,isOptional:S!=null}),S?"/?([^\\/]+)?":"/([^\\/]+)"));return o.endsWith("*")?(d.push({paramName:"*"}),p+=o==="*"||o==="/*"?"(.*)$":"(?:\\/(.+)|\\/*)$"):s?p+="\\/*$":o!==""&&o!=="/"&&(p+="(?:(?=\\/|$))"),[new RegExp(p,c?void 0:"i"),d]}function wp(o){try{return o.split("/").map(c=>decodeURIComponent(c).replace(/\//g,"%2F")).join("/")}catch(c){return ca(!1,'The URL path "'+o+'" could not be decoded because it is is a malformed URL segment. This is probably due to a bad percent '+("encoding ("+c+").")),o}}function Hn(o,c){if(c==="/")return o;if(!o.toLowerCase().startsWith(c.toLowerCase()))return null;let s=c.endsWith("/")?c.length-1:c.length,d=o.charAt(s);return d&&d!=="/"?null:o.slice(s)||"/"}const jp=/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i,kp=o=>jp.test(o);function _p(o,c){c===void 0&&(c="/");let{pathname:s,search:d="",hash:p=""}=typeof o=="string"?Qn(o):o,y;if(s)if(kp(s))y=s;else{if(s.includes("//")){let g=s;s=Ac(s),ca(!1,"Pathnames cannot have embedded double slashes - normalizing "+(g+" -> "+s))}s.startsWith("/")?y=wc(s.substring(1),"/"):y=wc(s,c)}else y=c;return{pathname:y,search:Ep(d),hash:Pp(p)}}function wc(o,c){let s=c.replace(/\/+$/,"").split("/");return o.split("/").forEach(p=>{p===".."?s.length>1&&s.pop():p!=="."&&s.push(p)}),s.length>1?s.join("/"):"/"}function la(o,c,s,d){return"Cannot include a '"+o+"' character in a manually specified "+("`to."+c+"` field ["+JSON.stringify(d)+"]. Please separate it out to the ")+("`to."+s+"` field. Alternatively you may provide the full path as ")+'a string in and the router will parse it for you.'}function Cp(o){return o.filter((c,s)=>s===0||c.route.path&&c.route.path.length>0)}function da(o,c){let s=Cp(o);return c?s.map((d,p)=>p===s.length-1?d.pathname:d.pathnameBase):s.map(d=>d.pathnameBase)}function fa(o,c,s,d){d===void 0&&(d=!1);let p;typeof o=="string"?p=Qn(o):(p=Wr({},o),ze(!p.pathname||!p.pathname.includes("?"),la("?","pathname","search",p)),ze(!p.pathname||!p.pathname.includes("#"),la("#","pathname","hash",p)),ze(!p.search||!p.search.includes("#"),la("#","search","hash",p)));let y=o===""||p.pathname==="",g=y?"/":p.pathname,P;if(g==null)P=s;else{let v=c.length-1;if(!d&&g.startsWith("..")){let L=g.split("/");for(;L[0]==="..";)L.shift(),v-=1;p.pathname=L.join("/")}P=v>=0?c[v]:"/"}let S=_p(p,P),T=g&&g!=="/"&&g.endsWith("/"),z=(y||g===".")&&s.endsWith("/");return!S.pathname.endsWith("/")&&(T||z)&&(S.pathname+="/"),S}const Ac=o=>o.replace(/\/\/+/g,"/"),ln=o=>Ac(o.join("/")),Np=o=>o.replace(/\/+$/,"").replace(/^\/*/,"/"),Ep=o=>!o||o==="?"?"":o.startsWith("?")?o:"?"+o,Pp=o=>!o||o==="#"?"":o.startsWith("#")?o:"#"+o;function zp(o){return o!=null&&typeof o.status=="number"&&typeof o.statusText=="string"&&typeof o.internal=="boolean"&&"data"in o}const $c=["post","put","patch","delete"];new Set($c);const Tp=["get",...$c];new Set(Tp);/** + * React Router v6.30.4 + * + * Copyright (c) Remix Software Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE.md file in the root directory of this source tree. + * + * @license MIT + */function Br(){return Br=Object.assign?Object.assign.bind():function(o){for(var c=1;c{P.current=!0}),m.useCallback(function(T,z){if(z===void 0&&(z={}),!P.current)return;if(typeof T=="number"){d.go(T);return}let v=fa(T,JSON.parse(g),y,z.relative==="path");o==null&&c!=="/"&&(v.pathname=v.pathname==="/"?c:ln([c,v.pathname])),(z.replace?d.replace:d.push)(v,z.state,z)},[c,d,g,y,o])}function pa(){let{matches:o}=m.useContext(Bt),c=o[o.length-1];return c?c.params:{}}function li(o,c){let{relative:s}=c===void 0?{}:c,{future:d}=m.useContext(Wt),{matches:p}=m.useContext(Bt),{pathname:y}=xn(),g=JSON.stringify(da(p,d.v7_relativeSplatPath));return m.useMemo(()=>fa(o,JSON.parse(g),y,s==="path"),[o,g,y,s])}function Lp(o,c){return Ip(o,c)}function Ip(o,c,s,d){Kn()||ze(!1);let{navigator:p}=m.useContext(Wt),{matches:y}=m.useContext(Bt),g=y[y.length-1],P=g?g.params:{};g&&g.pathname;let S=g?g.pathnameBase:"/";g&&g.route;let T=xn(),z;if(c){var v;let j=typeof c=="string"?Qn(c):c;S==="/"||(v=j.pathname)!=null&&v.startsWith(S)||ze(!1),z=j}else z=T;let L=z.pathname||"/",A=L;if(S!=="/"){let j=S.replace(/^\//,"").split("/");A="/"+L.replace(/^\//,"").split("/").slice(j.length).join("/")}let R=sp(o,{pathname:A}),N=Mp(R&&R.map(j=>Object.assign({},j,{params:Object.assign({},P,j.params),pathname:ln([S,p.encodeLocation?p.encodeLocation(j.pathname).pathname:j.pathname]),pathnameBase:j.pathnameBase==="/"?S:ln([S,p.encodeLocation?p.encodeLocation(j.pathnameBase).pathname:j.pathnameBase])})),y,s,d);return c&&N?m.createElement(ri.Provider,{value:{location:Br({pathname:"/",search:"",hash:"",state:null,key:"default"},z),navigationType:rn.Pop}},N):N}function Fp(){let o=Up(),c=zp(o)?o.status+" "+o.statusText:o instanceof Error?o.message:JSON.stringify(o),s=o instanceof Error?o.stack:null,p={padding:"0.5rem",backgroundColor:"rgba(200,200,200, 0.5)"};return m.createElement(m.Fragment,null,m.createElement("h2",null,"Unexpected Application Error!"),m.createElement("h3",{style:{fontStyle:"italic"}},c),s?m.createElement("pre",{style:p},s):null,null)}const Wp=m.createElement(Fp,null);class Bp extends m.Component{constructor(c){super(c),this.state={location:c.location,revalidation:c.revalidation,error:c.error}}static getDerivedStateFromError(c){return{error:c}}static getDerivedStateFromProps(c,s){return s.location!==c.location||s.revalidation!=="idle"&&c.revalidation==="idle"?{error:c.error,location:c.location,revalidation:c.revalidation}:{error:c.error!==void 0?c.error:s.error,location:s.location,revalidation:c.revalidation||s.revalidation}}componentDidCatch(c,s){console.error("React Router caught the following error during render",c,s)}render(){return this.state.error!==void 0?m.createElement(Bt.Provider,{value:this.props.routeContext},m.createElement(Vc.Provider,{value:this.state.error,children:this.props.component})):this.props.children}}function Op(o){let{routeContext:c,match:s,children:d}=o,p=m.useContext(ni);return p&&p.static&&p.staticContext&&(s.route.errorElement||s.route.ErrorBoundary)&&(p.staticContext._deepestRenderedBoundaryId=s.route.id),m.createElement(Bt.Provider,{value:c},d)}function Mp(o,c,s,d){var p;if(c===void 0&&(c=[]),s===void 0&&(s=null),d===void 0&&(d=null),o==null){var y;if(!s)return null;if(s.errors)o=s.matches;else if((y=d)!=null&&y.v7_partialHydration&&c.length===0&&!s.initialized&&s.matches.length>0)o=s.matches;else return null}let g=o,P=(p=s)==null?void 0:p.errors;if(P!=null){let z=g.findIndex(v=>v.route.id&&(P==null?void 0:P[v.route.id])!==void 0);z>=0||ze(!1),g=g.slice(0,Math.min(g.length,z+1))}let S=!1,T=-1;if(s&&d&&d.v7_partialHydration)for(let z=0;z=0?g=g.slice(0,T+1):g=[g[0]];break}}}return g.reduceRight((z,v,L)=>{let A,R=!1,N=null,j=null;s&&(A=P&&v.route.id?P[v.route.id]:void 0,N=v.route.errorElement||Wp,S&&(T<0&&L===0?(Hp("route-fallback"),R=!0,j=null):T===L&&(R=!0,j=v.route.hydrateFallbackElement||null)));let F=c.concat(g.slice(0,L+1)),_=()=>{let W;return A?W=N:R?W=j:v.route.Component?W=m.createElement(v.route.Component,null):v.route.element?W=v.route.element:W=z,m.createElement(Op,{match:v,routeContext:{outlet:z,matches:F,isDataRoute:s!=null},children:W})};return s&&(v.route.ErrorBoundary||v.route.errorElement||L===0)?m.createElement(Bp,{location:s.location,revalidation:s.revalidation,component:N,error:A,children:_(),routeContext:{outlet:null,matches:F,isDataRoute:!0}}):_()},null)}var Qc=(function(o){return o.UseBlocker="useBlocker",o.UseRevalidator="useRevalidator",o.UseNavigateStable="useNavigate",o})(Qc||{}),Kc=(function(o){return o.UseBlocker="useBlocker",o.UseLoaderData="useLoaderData",o.UseActionData="useActionData",o.UseRouteError="useRouteError",o.UseNavigation="useNavigation",o.UseRouteLoaderData="useRouteLoaderData",o.UseMatches="useMatches",o.UseRevalidator="useRevalidator",o.UseNavigateStable="useNavigate",o.UseRouteId="useRouteId",o})(Kc||{});function Dp(o){let c=m.useContext(ni);return c||ze(!1),c}function Ap(o){let c=m.useContext(Uc);return c||ze(!1),c}function $p(o){let c=m.useContext(Bt);return c||ze(!1),c}function Jc(o){let c=$p(),s=c.matches[c.matches.length-1];return s.route.id||ze(!1),s.route.id}function Up(){var o;let c=m.useContext(Vc),s=Ap(),d=Jc();return c!==void 0?c:(o=s.errors)==null?void 0:o[d]}function Vp(){let{router:o}=Dp(Qc.UseNavigateStable),c=Jc(Kc.UseNavigateStable),s=m.useRef(!1);return Hc(()=>{s.current=!0}),m.useCallback(function(p,y){y===void 0&&(y={}),s.current&&(typeof p=="number"?o.navigate(p):o.navigate(p,Br({fromRouteId:c},y)))},[o,c])}const jc={};function Hp(o,c,s){jc[o]||(jc[o]=!0)}function Qp(o,c){o==null||o.v7_startTransition,o==null||o.v7_relativeSplatPath}function Kp(o){let{to:c,replace:s,state:d,relative:p}=o;Kn()||ze(!1);let{future:y,static:g}=m.useContext(Wt),{matches:P}=m.useContext(Bt),{pathname:S}=xn(),T=Ge(),z=fa(c,da(P,y.v7_relativeSplatPath),S,p==="path"),v=JSON.stringify(z);return m.useEffect(()=>T(JSON.parse(v),{replace:s,state:d,relative:p}),[T,v,p,s,d]),null}function Fe(o){ze(!1)}function Jp(o){let{basename:c="/",children:s=null,location:d,navigationType:p=rn.Pop,navigator:y,static:g=!1,future:P}=o;Kn()&&ze(!1);let S=c.replace(/^\/*/,"/"),T=m.useMemo(()=>({basename:S,navigator:y,static:g,future:Br({v7_relativeSplatPath:!1},P)}),[S,P,y,g]);typeof d=="string"&&(d=Qn(d));let{pathname:z="/",search:v="",hash:L="",state:A=null,key:R="default"}=d,N=m.useMemo(()=>{let j=Hn(z,S);return j==null?null:{location:{pathname:j,search:v,hash:L,state:A,key:R},navigationType:p}},[S,z,v,L,A,R,p]);return N==null?null:m.createElement(Wt.Provider,{value:T},m.createElement(ri.Provider,{children:s,value:N}))}function Yp(o){let{children:c,location:s}=o;return Lp(aa(c),s)}new Promise(()=>{});function aa(o,c){c===void 0&&(c=[]);let s=[];return m.Children.forEach(o,(d,p)=>{if(!m.isValidElement(d))return;let y=[...c,p];if(d.type===m.Fragment){s.push.apply(s,aa(d.props.children,y));return}d.type!==Fe&&ze(!1),!d.props.index||!d.props.children||ze(!1);let g={id:d.props.id||y.join("-"),caseSensitive:d.props.caseSensitive,element:d.props.element,Component:d.props.Component,index:d.props.index,path:d.props.path,loader:d.props.loader,action:d.props.action,errorElement:d.props.errorElement,ErrorBoundary:d.props.ErrorBoundary,hasErrorBoundary:d.props.ErrorBoundary!=null||d.props.errorElement!=null,shouldRevalidate:d.props.shouldRevalidate,handle:d.props.handle,lazy:d.props.lazy};d.props.children&&(g.children=aa(d.props.children,y)),s.push(g)}),s}/** + * React Router DOM v6.30.4 + * + * Copyright (c) Remix Software Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE.md file in the root directory of this source tree. + * + * @license MIT + */function ei(){return ei=Object.assign?Object.assign.bind():function(o){for(var c=1;c{T&&kc?kc(()=>S(v)):S(v)},[S,T]);return m.useLayoutEffect(()=>g.listen(z),[g,z]),m.useEffect(()=>Qp(d),[d]),m.createElement(Jp,{basename:c,children:s,location:P.location,navigationType:P.action,navigator:g,future:d})}const lm=typeof window<"u"&&typeof window.document<"u"&&typeof window.document.createElement<"u",im=/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i,Gc=m.forwardRef(function(c,s){let{onClick:d,relative:p,reloadDocument:y,replace:g,state:P,target:S,to:T,preventScrollReset:z,viewTransition:v}=c,L=Yc(c,qp),{basename:A}=m.useContext(Wt),R,N=!1;if(typeof T=="string"&&im.test(T)&&(R=T,lm))try{let W=new URL(window.location.href),B=T.startsWith("//")?new URL(W.protocol+T):new URL(T),Z=Hn(B.pathname,A);B.origin===W.origin&&Z!=null?T=Z+B.search+B.hash:N=!0}catch{}let j=bp(T,{relative:p}),F=am(T,{replace:g,state:P,target:S,preventScrollReset:z,relative:p,viewTransition:v});function _(W){d&&d(W),W.defaultPrevented||F(W)}return m.createElement("a",ei({},L,{href:R||j,onClick:N||y?d:_,ref:s,target:S}))}),Ir=m.forwardRef(function(c,s){let{"aria-current":d="page",caseSensitive:p=!1,className:y="",end:g=!1,style:P,to:S,viewTransition:T,children:z}=c,v=Yc(c,Zp),L=li(S,{relative:v.relative}),A=xn(),R=m.useContext(Uc),{navigator:N,basename:j}=m.useContext(Wt),F=R!=null&&sm(L)&&T===!0,_=N.encodeLocation?N.encodeLocation(L).pathname:L.pathname,W=A.pathname,B=R&&R.navigation&&R.navigation.location?R.navigation.location.pathname:null;p||(W=W.toLowerCase(),B=B?B.toLowerCase():null,_=_.toLowerCase()),B&&j&&(B=Hn(B,j)||B);const Z=_!=="/"&&_.endsWith("/")?_.length-1:_.length;let fe=W===_||!g&&W.startsWith(_)&&W.charAt(Z)==="/",we=B!=null&&(B===_||!g&&B.startsWith(_)&&B.charAt(_.length)==="/"),xe={isActive:fe,isPending:we,isTransitioning:F},re=fe?d:void 0,oe;typeof y=="function"?oe=y(xe):oe=[y,fe?"active":null,we?"pending":null,F?"transitioning":null].filter(Boolean).join(" ");let pe=typeof P=="function"?P(xe):P;return m.createElement(Gc,ei({},v,{"aria-current":re,className:oe,ref:s,style:pe,to:S,viewTransition:T}),typeof z=="function"?z(xe):z)});var sa;(function(o){o.UseScrollRestoration="useScrollRestoration",o.UseSubmit="useSubmit",o.UseSubmitFetcher="useSubmitFetcher",o.UseFetcher="useFetcher",o.useViewTransitionState="useViewTransitionState"})(sa||(sa={}));var _c;(function(o){o.UseFetcher="useFetcher",o.UseFetchers="useFetchers",o.UseScrollRestoration="useScrollRestoration"})(_c||(_c={}));function om(o){let c=m.useContext(ni);return c||ze(!1),c}function am(o,c){let{target:s,replace:d,state:p,preventScrollReset:y,relative:g,viewTransition:P}=c===void 0?{}:c,S=Ge(),T=xn(),z=li(o,{relative:g});return m.useCallback(v=>{if(Xp(v,s)){v.preventDefault();let L=d!==void 0?d:Zl(T)===Zl(z);S(o,{replace:L,state:p,preventScrollReset:y,relative:g,viewTransition:P})}},[T,S,z,d,p,s,o,y,g,P])}function sm(o,c){c===void 0&&(c={});let s=m.useContext(tm);s==null&&ze(!1);let{basename:d}=om(sa.useViewTransitionState),p=li(o,{relative:c.relative});if(!s.isTransitioning)return!1;let y=Hn(s.currentLocation.pathname,d)||s.currentLocation.pathname,g=Hn(s.nextLocation.pathname,d)||s.nextLocation.pathname;return oa(p.pathname,g)!=null||oa(p.pathname,y)!=null}const um="/api";function cm(){return localStorage.getItem("fitness_token")}async function K(o,c={}){const s=cm(),d={"Content-Type":"application/json",...c.headers};s&&(d.Authorization=`Bearer ${s}`);const p=await fetch(`${um}${o}`,{...c,headers:d});if(p.status===401)throw localStorage.removeItem("fitness_token"),window.location.href="/login",new Error("Unauthorized");if(!p.ok){const y=await p.json().catch(()=>({detail:"Request failed"}));throw new Error(y.detail||`HTTP ${p.status}`)}return p.json()}const J={login:(o,c)=>K("/auth/login",{method:"POST",body:JSON.stringify({username:o,password:c})}),register:(o,c,s)=>K("/auth/register",{method:"POST",body:JSON.stringify({username:o,password:c,display_name:s})}),me:()=>K("/auth/me"),onboardingStatus:()=>K("/onboarding/status"),savePhase1:o=>K("/onboarding/phase1",{method:"POST",body:JSON.stringify(o)}),savePhase2:o=>K("/onboarding/phase2",{method:"POST",body:JSON.stringify(o)}),getRecommendations:()=>K("/onboarding/recommendations"),today:()=>K("/points/today"),week:o=>K(`/points/week${o?`?start=${o}`:""}`),getRules:()=>K("/points/rules"),updateRule:(o,c)=>K(`/points/rules/${o}`,{method:"PATCH",body:JSON.stringify(c)}),getTargets:()=>K("/points/targets"),updateTargets:o=>K("/points/targets",{method:"PATCH",body:JSON.stringify(o)}),listActivities:o=>K(`/activities${o?`?date=${o}`:""}`),logActivity:o=>K("/activities",{method:"POST",body:JSON.stringify(o)}),deleteActivity:o=>K(`/activities/${o}`,{method:"DELETE"}),listFood:o=>K(`/food${o?`?date=${o}`:""}`),logFood:o=>K("/food",{method:"POST",body:JSON.stringify(o)}),deleteFood:o=>K(`/food/${o}`,{method:"DELETE"}),scanBarcode:o=>K(`/food/scan/${o}`),searchFood:o=>K(`/food/search?q=${encodeURIComponent(o)}`),nutritionToday:()=>K("/nutrition/today"),getNutritionGoals:()=>K("/nutrition/goals"),updateNutritionGoals:o=>K("/nutrition/goals",{method:"PATCH",body:JSON.stringify(o)}),startFast:o=>K("/nutrition/fasts",{method:"POST",body:JSON.stringify(o)}),endFast:(o,c)=>K(`/nutrition/fasts/${o}`,{method:"PATCH",body:JSON.stringify(c||{})}),listFasts:o=>K(`/nutrition/fasts${o?`?limit=${o}`:""}`),getActiveFast:()=>K("/nutrition/fasts/active"),deleteFast:o=>K(`/nutrition/fasts/${o}`,{method:"DELETE"}),logElectrolytes:o=>K("/nutrition/electrolytes",{method:"POST",body:JSON.stringify(o)}),getElectrolytesToday:()=>K("/nutrition/electrolytes/today"),listRewards:()=>K("/rewards"),createReward:o=>K("/rewards",{method:"POST",body:JSON.stringify(o)}),redeemReward:(o,c)=>K(`/rewards/${o}/redeem`,{method:"POST",body:JSON.stringify({date:c})}),listPrograms:()=>K("/programs"),createProgram:o=>K("/programs",{method:"POST",body:JSON.stringify(o)}),getProgram:o=>K(`/programs/${o}`),searchCatalog:o=>K(`/programs/catalog${o?`?q=${encodeURIComponent(o)}`:""}`),getCatalogProgram:o=>K(`/programs/catalog/${o}`),researchProgram:o=>K("/programs/research",{method:"POST",body:JSON.stringify({name:o})}),adoptProgram:(o,c)=>K(`/programs/catalog/${o}/adopt`,{method:"POST",body:JSON.stringify({start_date:c})}),getProgramSchedule:o=>K(`/programs/${o}/schedule`),getWorkoutDetail:(o,c)=>K(`/programs/${o}/workout/${c}`),completeWorkout:(o,c,s)=>K(`/programs/${o}/workout/${c}/complete`,{method:"POST",body:JSON.stringify(s)}),getProgramProgress:o=>K(`/programs/${o}/progress`),getThread:()=>K("/support/thread"),sendSupportMessage:o=>K("/support/messages",{method:"POST",body:JSON.stringify({body:o})}),getUnreadCount:()=>K("/support/unread"),listSupportThreads:()=>K("/support/admin/threads"),getAdminThread:o=>K(`/support/admin/threads/${o}`),replySupportThread:(o,c)=>K(`/support/admin/threads/${o}/reply`,{method:"POST",body:JSON.stringify({body:c})}),integrationStatus:()=>K("/integrations"),stravaAuth:()=>K("/integrations/strava/auth"),stravaSync:()=>K("/integrations/strava/sync",{method:"POST"}),polarAuth:()=>K("/integrations/polar/auth"),polarSync:()=>K("/integrations/polar/sync",{method:"POST"}),disconnect:o=>K(`/integrations/${o}`,{method:"DELETE"}),listMealPlans:()=>K("/meal-plans"),createMealPlan:o=>K("/meal-plans",{method:"POST",body:JSON.stringify(o)}),getMealPlanToday:()=>K("/meal-plans/today"),getMealPlan:o=>K("/meal-plans/"+o),updateMealPlanStatus:(o,c)=>K("/meal-plans/"+o,{method:"PATCH",body:JSON.stringify({status:c})}),logMealCompliance:o=>K("/meal-plans/compliance",{method:"POST",body:JSON.stringify(o)}),getMealPlanProgress:o=>K("/meal-plans/"+o+"/progress"),fullIntegrationStatus:()=>K("/integrations/status"),listProviders:()=>K("/integrations/providers"),configureIntegration:(o,c)=>K("/integrations/configure",{method:"POST",body:JSON.stringify({provider:o,credentials:c})}),getAIStatus:()=>K("/ai/status"),getResourceRecommendations:()=>K("/onboarding/recommendations")};function dm(o){localStorage.setItem("fitness_token",o)}function fm(){localStorage.removeItem("fitness_token")}function ti(){return!!localStorage.getItem("fitness_token")}function pm(){const[o,c]=m.useState("login"),[s,d]=m.useState(""),[p,y]=m.useState(""),[g,P]=m.useState(""),[S,T]=m.useState(""),[z,v]=m.useState(!1),L=Ge();async function A(R){R.preventDefault(),T(""),v(!0);try{const N=o==="login"?await J.login(s,p):await J.register(s,p,g||s);dm(N.access_token);const j=await J.onboardingStatus();L(j.phase1_completed?"/":"/onboarding")}catch(N){T(N.message)}finally{v(!1)}}return l.jsx("div",{style:{minHeight:"100dvh",display:"flex",flexDirection:"column",alignItems:"center",justifyContent:"center",padding:"24px",background:"linear-gradient(170deg, var(--accent-light) 0%, var(--accent) 30%, var(--accent-dark) 60%, var(--text) 100%)"},children:l.jsxs("div",{style:{width:"100%",maxWidth:"380px"},children:[l.jsxs("div",{style:{textAlign:"center",marginBottom:"32px",color:"#fff"},children:[l.jsx("div",{style:{fontSize:"3.2rem",marginBottom:"8px",filter:"drop-shadow(0 4px 12px rgba(0,0,0,0.2))"},children:"🔥"}),l.jsx("h1",{style:{fontFamily:"var(--font-display)",fontSize:"2.2rem",fontWeight:900,letterSpacing:"-0.03em"},children:"Fitness Rewards"}),l.jsx("p",{style:{opacity:.8,marginTop:"4px",fontSize:"0.95rem",fontWeight:500},children:"Earn your rewards. Every single day."})]}),l.jsxs("div",{style:{background:"var(--card)",borderRadius:"var(--r-lg)",padding:"28px",boxShadow:"0 20px 60px rgba(0,0,0,0.3)"},children:[l.jsx("div",{style:{display:"grid",gridTemplateColumns:"1fr 1fr",gap:"4px",background:"rgba(0,0,0,0.04)",borderRadius:"var(--r)",padding:"4px",marginBottom:"24px"},children:["login","register"].map(R=>l.jsx("button",{type:"button",onClick:()=>c(R),style:{borderRadius:"var(--r-sm)",padding:"10px",fontWeight:700,fontSize:"0.85rem",background:o===R?"var(--accent)":"transparent",color:o===R?"#fff":"var(--text-2)",boxShadow:o===R?"var(--shadow-accent)":"none"},children:R==="login"?"Sign In":"Sign Up"},R))}),S&&l.jsx("div",{className:"error-msg",children:S}),l.jsxs("form",{onSubmit:A,children:[l.jsxs("div",{className:"form-group",children:[l.jsx("label",{className:"form-label",children:"Username"}),l.jsx("input",{value:s,onChange:R=>d(R.target.value),required:!0,autoComplete:"username"})]}),l.jsxs("div",{className:"form-group",children:[l.jsx("label",{className:"form-label",children:"Password"}),l.jsx("input",{type:"password",value:p,onChange:R=>y(R.target.value),required:!0,autoComplete:"current-password"})]}),o==="register"&&l.jsxs("div",{className:"form-group",children:[l.jsx("label",{className:"form-label",children:"Display Name"}),l.jsx("input",{value:g,onChange:R=>P(R.target.value),placeholder:"What should we call you?"})]}),l.jsx("button",{type:"submit",className:"btn-primary btn-full",disabled:z,style:{padding:"14px",fontSize:"0.95rem",marginTop:"8px",borderRadius:"var(--r)"},children:z?"...":o==="login"?"Sign In":"Create Account"})]})]})]})})}function Yl({text:o,children:c}){const[s,d]=m.useState(!1);return l.jsxs("span",{style:{position:"relative",display:"inline-flex",alignItems:"center"},children:[c,l.jsx("span",{onMouseEnter:()=>d(!0),onMouseLeave:()=>d(!1),onClick:p=>{p.stopPropagation(),d(y=>!y)},style:{display:"inline-flex",alignItems:"center",justifyContent:"center",width:"18px",height:"18px",borderRadius:"50%",marginLeft:"6px",background:"rgba(0,0,0,0.06)",color:"var(--text-3)",cursor:"help",fontSize:"0.7rem",fontWeight:800,flexShrink:0},children:"?"}),s&&l.jsx("span",{style:{position:"absolute",bottom:"calc(100% + 8px)",left:"50%",transform:"translateX(-50%)",background:"var(--text)",color:"#fff",padding:"10px 14px",borderRadius:"var(--r-sm)",fontSize:"0.78rem",lineHeight:1.45,fontWeight:500,width:"240px",boxShadow:"var(--shadow-3)",zIndex:50,pointerEvents:"none"},children:o})]})}function mm(){var _,W;const[o,c]=m.useState(null),[s,d]=m.useState(null),[p,y]=m.useState(!0),[g,P]=m.useState(null),S=Ge();m.useEffect(()=>{T()},[]);async function T(){try{const[B,Z]=await Promise.all([J.today(),J.integrationStatus()]);c(B),d(Z)}catch(B){console.error(B)}finally{y(!1)}}async function z(B){P(B);try{const Z=await(B==="strava"?J.stravaSync:J.polarSync)();await T(),Z.imported>0?alert(`Synced ${Z.imported} activities from ${B}!`):alert(`No new activities found on ${B}.`)}catch(Z){alert(`Sync failed: ${Z.message}`)}finally{P(null)}}async function v(B){try{const Z=await(B==="strava"?J.stravaAuth:J.polarAuth)();window.location.href=Z.auth_url}catch(Z){alert(`Connect failed: ${Z.message}`)}}if(p)return l.jsx("div",{className:"page",children:l.jsx("div",{className:"loading",children:"Loading..."})});if(!o)return l.jsx("div",{className:"page",children:l.jsx("div",{className:"error-msg",children:"Failed to load"})});const L=o.daily_minimum>0?Math.min(100,Math.round(o.points_earned/o.daily_minimum*100)):0,A=o.weekly_target>0?Math.min(100,Math.round(o.week_points/o.weekly_target*100)):0,R=new Set(o.activities_today.map(B=>B.category)),N=(_=s==null?void 0:s.strava)==null?void 0:_.connected,j=(W=s==null?void 0:s.polar)==null?void 0:W.connected,F=[{key:"workout",label:"Workout",icon:"💪",pts:50,color:"var(--accent)",tip:"Any exercise session — running, weights, yoga, etc. Logged manually or synced from Strava/Polar."},{key:"food_log",label:"Food logged",icon:"🥗",pts:30,color:"var(--green)",tip:"Log what you eat. Barcode scan or manual entry. Building food awareness is a key habit."},{key:"steps_target",label:"Steps target",icon:"👟",pts:20,color:"#2979FF",tip:"Hit your daily step goal. Steps are the foundation of an active lifestyle."},{key:"screen_free",label:"Screen-free",icon:"📖",pts:"20/hr",color:"#7C4DFF",tip:"Time away from screens — reading, walking, hobbies. Points scale with hours logged."},{key:"daily_checkin",label:"Check-in",icon:"✅",pts:10,color:"#00BCD4",tip:"Just show up and check in. The easiest points — consistency matters more than intensity."}];return l.jsxs("div",{className:"page",children:[o.program_name&&l.jsxs("div",{onClick:()=>o.program_id&&S(`/programs/${o.program_id}`),style:{background:"var(--accent-bg)",borderRadius:"var(--r)",padding:"12px 16px",marginBottom:"14px",display:"flex",alignItems:"center",gap:"12px",border:"1px solid rgba(41,121,255,0.1)",cursor:o.program_id?"pointer":"default"},children:[l.jsx("div",{style:{fontSize:"1.1rem"},children:"📋"}),l.jsxs("div",{style:{flex:1},children:[l.jsx("div",{style:{fontSize:"0.78rem",fontWeight:700,color:"var(--accent)"},children:o.program_name}),l.jsx("div",{className:"progress-bar",style:{height:"5px",marginTop:"4px"},children:l.jsx("div",{className:"progress-bar-fill",style:{width:`${Math.round(o.program_day/o.program_total_days*100)}%`,background:"var(--accent)"}})})]}),l.jsxs("div",{style:{fontFamily:"var(--font-display)",fontWeight:800,fontSize:"0.85rem",color:"var(--accent)",whiteSpace:"nowrap"},children:[o.program_day,"/",o.program_total_days]})]}),l.jsxs("div",{className:`gate-banner ${o.gate_passed?"gate-earned":"gate-locked"}`,children:[l.jsx("div",{className:"section-label",style:{marginBottom:"4px"},children:l.jsx(Yl,{text:`Hit ${o.daily_minimum} points to unlock your rewards for today. Points reset weekly. This is your daily "gate" — earn first, enjoy second.`,children:"Today"})}),l.jsxs("div",{className:"gate-pts",children:[l.jsx("span",{style:{color:o.gate_passed?"var(--green)":"var(--accent)"},children:o.points_earned}),l.jsxs("span",{style:{fontFamily:"var(--font)",fontSize:"1rem",fontWeight:600,color:"var(--text-3)"},children:["/",o.daily_minimum]})]}),l.jsx("div",{className:"progress-bar",style:{marginTop:"12px",height:"12px"},children:l.jsx("div",{className:"progress-bar-fill",style:{width:`${L}%`,background:o.gate_passed?"linear-gradient(90deg, #00C853, #69F0AE)":"linear-gradient(90deg, var(--accent), var(--accent-light))"}})}),l.jsx("div",{style:{marginTop:"12px",fontSize:"0.88rem",fontWeight:700},children:o.gate_passed?l.jsx("span",{style:{color:"var(--green-dark)"},children:"✨ Rewards unlocked!"}):l.jsxs("span",{style:{color:"var(--text-2)"},children:["🔒 ",o.points_remaining," more to unlock"]})})]}),l.jsxs("div",{style:{display:"grid",gridTemplateColumns:"1fr 1fr",gap:"10px",marginBottom:"14px"},children:[l.jsx("button",{className:"btn-primary btn-full",onClick:()=>S("/log"),style:{padding:"14px",fontSize:"0.9rem"},children:"+ Log Activity"}),l.jsx("button",{className:"btn-outline btn-full",onClick:()=>S("/food"),style:{padding:"14px",fontSize:"0.9rem"},children:"🍽️ Log Food"})]}),l.jsxs("div",{className:"card",children:[l.jsx("div",{className:"section-label",children:"Activities"}),F.map(B=>{const Z=R.has(B.key);return l.jsxs("div",{className:"checklist-item",style:{opacity:Z?1:.6},children:[l.jsx("span",{style:{width:"32px",height:"32px",borderRadius:"8px",background:Z?B.color:"rgba(0,0,0,0.04)",display:"flex",alignItems:"center",justifyContent:"center",fontSize:"0.9rem",transition:"all 0.3s"},children:Z?"✓":B.icon}),l.jsx("span",{style:{fontWeight:600,fontSize:"0.88rem"},children:l.jsx(Yl,{text:B.tip,children:B.label})}),l.jsxs("span",{className:"checklist-points",children:["+",B.pts]})]},B.key)})]}),l.jsxs("div",{className:"card",style:{background:"var(--accent-bg)"},children:[l.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:"10px"},children:[l.jsx("div",{className:"section-label",style:{margin:0},children:l.jsx(Yl,{text:`Earn ${o.weekly_target} total points across the week for a bonus. Consistent daily effort beats one big day.`,children:"This Week"})}),l.jsx(Gc,{to:"/week",style:{fontSize:"0.8rem"},children:"Details →"})]}),l.jsxs("div",{style:{display:"flex",alignItems:"baseline",gap:"4px",marginBottom:"8px"},children:[l.jsx("span",{style:{fontFamily:"var(--font-display)",fontWeight:900,fontSize:"1.4rem"},children:o.week_points}),l.jsxs("span",{style:{fontSize:"0.85rem",color:"var(--text-3)",fontWeight:600},children:["/ ",o.weekly_target," pts"]})]}),l.jsx("div",{className:"progress-bar",children:l.jsx("div",{className:"progress-bar-fill",style:{width:`${A}%`,background:"linear-gradient(90deg, #7C4DFF, #B388FF)"}})})]}),o.gate_passed&&o.rewards_available.length>0&&l.jsxs("div",{className:"card",style:{background:"var(--green-bg)",border:"2px solid rgba(0,200,83,0.15)"},children:[l.jsx("div",{className:"section-label",style:{color:"var(--green-dark)"},children:"🎮 Rewards Available"}),o.rewards_available.map(B=>l.jsxs("div",{className:"reward-card",children:[l.jsxs("div",{children:[l.jsx("div",{style:{fontWeight:700},children:B.name}),l.jsxs("div",{style:{fontSize:"0.78rem",color:"var(--text-3)",fontWeight:600},children:[B.point_cost," pts"]})]}),l.jsx("button",{className:"btn-success btn-sm",onClick:async()=>{try{await J.redeemReward(B.id),await T()}catch(Z){alert(Z.message)}},children:"Redeem"})]},B.id))]}),l.jsxs("div",{className:"card",children:[l.jsx("div",{className:"section-label",children:l.jsx(Yl,{text:"Connect your fitness trackers to automatically import workouts. Go to Settings → Integrations to connect.",children:"Integrations"})}),l.jsxs("div",{style:{display:"grid",gridTemplateColumns:"1fr 1fr",gap:"8px"},children:[N?l.jsx("button",{className:"btn-outline btn-sm btn-full",onClick:()=>z("strava"),disabled:g==="strava",children:g==="strava"?"⏳ Syncing...":"🔄 Sync Strava"}):l.jsx("button",{className:"btn-ghost btn-sm btn-full",onClick:()=>v("strava"),style:{border:"2px dashed var(--divider)"},children:"🔗 Connect Strava"}),j?l.jsx("button",{className:"btn-outline btn-sm btn-full",onClick:()=>z("polar"),disabled:g==="polar",children:g==="polar"?"⏳ Syncing...":"🔄 Sync Polar"}):l.jsx("button",{className:"btn-ghost btn-sm btn-full",onClick:()=>v("polar"),style:{border:"2px dashed var(--divider)"},children:"🔗 Connect Polar"})]})]})]})}const Cc=[{value:"workout",label:"Workout",icon:"💪",desc:"Any exercise session",color:"var(--accent)"},{value:"steps_target",label:"Steps",icon:"👟",desc:"Hit your daily goal",color:"#2979FF"},{value:"screen_free",label:"Screen-Free",icon:"📖",desc:"Reading, outdoors",color:"#7C4DFF"},{value:"daily_checkin",label:"Check-in",icon:"✅",desc:"Just show up",color:"#00BCD4"}];function hm(){const[o,c]=m.useState(""),[s,d]=m.useState(""),[p,y]=m.useState(""),[g,P]=m.useState(""),[S,T]=m.useState(!1),[z,v]=m.useState(""),[L,A]=m.useState(null),[R,N]=m.useState(null),[j,F]=m.useState([]),[_,W]=m.useState(null),B=Ge();m.useEffect(()=>{Z()},[]);async function Z(){try{const oe=(await J.listPrograms()||[]).find(ve=>ve.status==="active"&&ve.catalog_id);if(!oe)return;A(oe);const pe=await J.getProgramSchedule(oe.id);pe.today_workout&&!pe.today_workout.rest_day&&!pe.today_workout.completed&&N(pe.today_workout);const Ne=(pe.schedule||[]).filter(ve=>{var Te;return ve.week_number===pe.current_week&&!ve.rest_day&&!ve.completed&&ve.id!==((Te=pe.today_workout)==null?void 0:Te.id)}).slice(0,3);F(Ne)}catch(re){console.warn("Could not load active program:",re.message)}}async function fe(re){if(!(!L||!re||_)){W(re.id);try{const oe=await J.completeWorkout(L.id,re.id,{});let Ne=`+${oe.total_points||oe.points_earned} points!`;oe.weekly_bonus>0&&(Ne+=" (week bonus!)"),oe.completion_bonus>0&&(Ne+=" (program complete!)"),v(Ne),setTimeout(()=>B("/"),1500)}catch(oe){alert(oe.message),W(null)}}}async function we(re){var oe;if(re.preventDefault(),!!o){T(!0);try{const pe=new Date().toISOString().split("T")[0],Ne=await J.logActivity({category:o,title:s||((oe=Cc.find(ve=>ve.value===o))==null?void 0:oe.label),description:g||null,duration_minutes:p?parseInt(p):null,activity_date:pe});v(`+${Ne.points_earned} points!`),c(""),d(""),y(""),P(""),setTimeout(()=>B("/"),1200)}catch(pe){alert(pe.message)}finally{T(!1)}}}const xe=o==="workout"&&L;return l.jsxs("div",{className:"page",children:[l.jsx("h1",{className:"page-title",children:"Log Activity"}),z&&l.jsxs("div",{style:{textAlign:"center",padding:"24px",marginBottom:"14px",background:"var(--green-bg)",borderRadius:"var(--r-lg)",border:"2px solid rgba(0,200,83,0.15)"},children:[l.jsx("div",{style:{fontSize:"2rem",marginBottom:"4px"},children:"🎉"}),l.jsx("div",{style:{fontFamily:"var(--font-display)",fontWeight:900,fontSize:"1.3rem",color:"var(--green-dark)"},children:z})]}),l.jsxs("div",{style:{marginBottom:"18px"},children:[l.jsx("div",{className:"section-label",children:"What did you do?"}),l.jsx("div",{className:"option-grid",children:Cc.map(re=>l.jsxs("div",{className:`option-btn ${o===re.value?"selected":""}`,onClick:()=>c(re.value),style:o===re.value?{borderColor:re.color,background:re.color+"0A"}:{},children:[l.jsx("div",{style:{fontSize:"1.5rem",marginBottom:"6px"},children:re.icon}),l.jsx("div",{style:{fontWeight:800,fontSize:"0.85rem"},children:re.label}),l.jsx("div",{style:{fontSize:"0.72rem",color:"var(--text-3)",marginTop:"2px",fontWeight:400},children:re.desc})]},re.value))})]}),xe&&l.jsxs("div",{style:{animation:"fadeUp 0.25s ease-out",marginBottom:"18px"},children:[l.jsxs("div",{onClick:()=>B(`/programs/${L.id}`),style:{background:"var(--accent-bg)",borderRadius:"var(--r-sm)",padding:"10px 14px",marginBottom:"12px",display:"flex",alignItems:"center",justifyContent:"space-between",cursor:"pointer",border:"1px solid rgba(41,121,255,0.15)"},children:[l.jsxs("div",{children:[l.jsx("div",{style:{fontSize:"0.7rem",fontWeight:700,color:"var(--accent)",textTransform:"uppercase",letterSpacing:"0.05em"},children:"Active Program"}),l.jsx("div",{style:{fontWeight:800,fontSize:"0.92rem",color:"var(--text-1)"},children:L.name})]}),l.jsxs("div",{style:{fontSize:"0.78rem",color:"var(--accent)",fontWeight:700},children:["Week ",L.current_week||1," →"]})]}),R&&l.jsx(Nc,{workout:R,featured:!0,completing:_===R.id,onComplete:()=>fe(R)}),j.length>0&&l.jsxs(l.Fragment,{children:[l.jsx("div",{className:"section-label",style:{marginTop:"14px",marginBottom:"8px"},children:R?"Or another from this week":"Workouts this week"}),j.map(re=>l.jsx(Nc,{workout:re,completing:_===re.id,onComplete:()=>fe(re)},re.id))]}),!R&&j.length===0&&l.jsx("div",{style:{padding:"14px",borderRadius:"var(--r-sm)",background:"var(--green-bg)",color:"var(--green-dark)",fontSize:"0.85rem",textAlign:"center",fontWeight:600},children:"✓ All workouts for this week are complete. Log a freeform workout below or rest up."}),l.jsx("div",{style:{textAlign:"center",color:"var(--text-3)",fontSize:"0.78rem",margin:"14px 0 6px",textTransform:"uppercase",letterSpacing:"0.06em",fontWeight:700},children:"— or log freeform —"})]}),o&&l.jsxs("form",{onSubmit:we,className:"card",style:{animation:"fadeUp 0.25s ease-out"},children:[l.jsxs("div",{className:"form-group",children:[l.jsx("label",{className:"form-label",children:"Title (optional)"}),l.jsx("input",{value:s,onChange:re=>d(re.target.value),placeholder:"e.g. Morning run, evening yoga"})]}),(o==="workout"||o==="screen_free")&&l.jsxs("div",{className:"form-group",children:[l.jsx("label",{className:"form-label",children:"Duration (minutes)"}),l.jsx("input",{type:"number",value:p,onChange:re=>y(re.target.value),placeholder:"30"})]}),l.jsxs("div",{className:"form-group",children:[l.jsx("label",{className:"form-label",children:"Notes (optional)"}),l.jsx("textarea",{value:g,onChange:re=>P(re.target.value),rows:2,placeholder:"How did it go?"})]}),l.jsx("button",{type:"submit",className:"btn-primary btn-full",disabled:S,style:{borderRadius:"var(--r)"},children:S?"Saving...":"Log Activity"})]})]})}function Nc({workout:o,featured:c,completing:s,onComplete:d}){return l.jsxs("div",{style:{background:"var(--card)",borderRadius:"var(--r)",padding:"14px",marginBottom:"10px",boxShadow:"var(--shadow-1)",border:c?"2px solid var(--accent-glow)":"1px solid var(--divider)"},children:[c&&l.jsx("div",{style:{fontSize:"0.65rem",fontWeight:800,color:"var(--accent)",textTransform:"uppercase",letterSpacing:"0.06em",marginBottom:"4px"},children:"⭐ Today"}),l.jsx("div",{style:{fontFamily:"var(--font-display)",fontWeight:800,fontSize:"1rem",marginBottom:"2px"},children:o.workout_name||`Day ${o.day_number}`}),l.jsxs("div",{style:{fontSize:"0.74rem",color:"var(--text-3)",marginBottom:"10px"},children:["Week ",o.week_number," · Day ",o.day_number," · ",(o.exercises||[]).length," exercises"]}),l.jsx("div",{style:{background:"var(--bg-warm)",borderRadius:"var(--r-sm)",padding:"8px 10px",marginBottom:"10px"},children:(o.exercises||[]).slice(0,4).map((p,y)=>l.jsxs("div",{style:{display:"flex",justifyContent:"space-between",fontSize:"0.76rem",padding:"2px 0",color:"var(--text-2)"},children:[l.jsx("span",{children:p.name}),l.jsx("span",{style:{color:"var(--text-3)",fontWeight:600},children:p.sets&&p.reps&&`${p.sets}×${p.reps}`})]},y))}),l.jsx("button",{onClick:d,disabled:s,style:{width:"100%",background:s?"var(--text-3)":"var(--green)",color:"#fff",padding:"11px",fontWeight:800,fontSize:"0.88rem",boxShadow:s?"none":"var(--shadow-green)"},children:s?"Logging...":"✓ Complete — 75 pts"})]})}const Ec=["breakfast","lunch","dinner","snack"];function gm(){const[o,c]=m.useState("log"),[s,d]=m.useState("lunch"),[p,y]=m.useState(""),[g,P]=m.useState(""),[S,T]=m.useState(""),[z,v]=m.useState(""),[L,A]=m.useState(""),[R,N]=m.useState(""),[j,F]=m.useState("1"),[_,W]=m.useState(""),[B,Z]=m.useState(""),[fe,we]=m.useState([]),[xe,re]=m.useState(null),[oe,pe]=m.useState(!1),[Ne,ve]=m.useState("");m.useEffect(()=>{Te()},[]);async function Te(){try{const I=new Date().toISOString().split("T")[0],V=await J.listFood(I);re(V)}catch(I){console.error(I)}}function Me(I){y(I.product_name||""),P(I.brand||""),T(I.calories_100g?String(Math.round(I.calories_100g)):""),v(I.protein_100g?String(Math.round(I.protein_100g)):""),A(I.carbs_100g?String(Math.round(I.carbs_100g)):""),N(I.fat_100g?String(Math.round(I.fat_100g)):""),c("log")}async function We(){if(_.trim()){pe(!0);try{const I=await J.scanBarcode(_.trim());Me(I)}catch{alert("Product not found. Try manual entry.")}finally{pe(!1)}}}async function Se(){if(B.trim()){pe(!0);try{const I=await J.searchFood(B);we(I.results||[])}catch(I){alert(I.message)}finally{pe(!1)}}}async function H(I){if(I.preventDefault(),!!p.trim()){pe(!0);try{const V=new Date().toISOString().split("T")[0];await J.logFood({meal_type:s,food_name:p,brand:g||null,calories:S?parseFloat(S)*parseFloat(j||1):null,protein_g:z?parseFloat(z)*parseFloat(j||1):null,carbs_g:L?parseFloat(L)*parseFloat(j||1):null,fat_g:R?parseFloat(R)*parseFloat(j||1):null,servings:parseFloat(j||1),food_date:V}),ve("Food logged!"),y(""),P(""),T(""),v(""),A(""),N(""),F("1"),await Te(),setTimeout(()=>ve(""),2e3)}catch(V){alert(V.message)}finally{pe(!1)}}}return l.jsxs("div",{className:"page",children:[l.jsx("h1",{className:"page-title",children:"Food Log"}),Ne&&l.jsx("div",{style:{padding:"12px",background:"var(--green-bg)",borderRadius:"var(--radius-sm)",color:"var(--green-dark)",textAlign:"center",marginBottom:"12px",fontWeight:600},children:Ne}),l.jsxs("div",{style:{display:"flex",gap:"8px",marginBottom:"16px"},children:[l.jsx("button",{className:o==="log"?"btn-primary btn-sm":"btn-outline btn-sm",onClick:()=>c("log"),children:"Manual"}),l.jsx("button",{className:o==="scan"?"btn-primary btn-sm":"btn-outline btn-sm",onClick:()=>c("scan"),children:"📷 Scan"}),l.jsx("button",{className:o==="search"?"btn-primary btn-sm":"btn-outline btn-sm",onClick:()=>c("search"),children:"🔍 Search"})]}),o==="scan"&&l.jsxs("div",{className:"card",children:[l.jsxs("div",{className:"form-group",children:[l.jsx("label",{className:"form-label",children:"Barcode number"}),l.jsx("input",{value:_,onChange:I=>W(I.target.value),placeholder:"Enter or scan barcode",inputMode:"numeric"})]}),l.jsx("button",{className:"btn-primary btn-full",onClick:We,disabled:oe,children:oe?"Looking up...":"Lookup"}),l.jsx("p",{style:{fontSize:"0.8rem",color:"var(--text-3)",marginTop:"8px",textAlign:"center"},children:"Powered by Open Food Facts (4M+ products)"})]}),o==="search"&&l.jsxs("div",{className:"card",children:[l.jsxs("div",{style:{display:"flex",gap:"8px",marginBottom:"12px"},children:[l.jsx("input",{value:B,onChange:I=>Z(I.target.value),placeholder:"Search foods...",onKeyDown:I=>I.key==="Enter"&&Se()}),l.jsx("button",{className:"btn-primary btn-sm",onClick:Se,disabled:oe,children:"Go"})]}),fe.map((I,V)=>l.jsxs("div",{style:{padding:"10px 0",borderBottom:"1px solid var(--divider)",cursor:"pointer"},onClick:()=>Me(I),children:[l.jsx("div",{style:{fontWeight:600,fontSize:"0.9rem"},children:I.product_name||"Unknown"}),l.jsxs("div",{style:{fontSize:"0.8rem",color:"var(--text-3)"},children:[I.brand&&`${I.brand} · `,I.calories_100g?`${Math.round(I.calories_100g)} cal/100g`:""]})]},V))]}),o==="log"&&l.jsx("form",{onSubmit:H,children:l.jsxs("div",{className:"card",children:[l.jsxs("div",{className:"form-group",children:[l.jsx("label",{className:"form-label",children:"Meal"}),l.jsx("div",{style:{display:"flex",gap:"6px"},children:Ec.map(I=>l.jsx("button",{type:"button",className:s===I?"btn-primary btn-sm":"btn-outline btn-sm",onClick:()=>d(I),children:I.charAt(0).toUpperCase()+I.slice(1)},I))})]}),l.jsxs("div",{className:"form-group",children:[l.jsx("label",{className:"form-label",children:"Food name"}),l.jsx("input",{value:p,onChange:I=>y(I.target.value),placeholder:"e.g. Grilled chicken breast",required:!0})]}),l.jsxs("div",{className:"form-group",children:[l.jsx("label",{className:"form-label",children:"Brand (optional)"}),l.jsx("input",{value:g,onChange:I=>P(I.target.value),placeholder:""})]}),l.jsxs("div",{style:{display:"grid",gridTemplateColumns:"1fr 1fr",gap:"10px"},children:[l.jsxs("div",{className:"form-group",children:[l.jsx("label",{className:"form-label",children:"Calories"}),l.jsx("input",{type:"number",value:S,onChange:I=>T(I.target.value),placeholder:"per serving"})]}),l.jsxs("div",{className:"form-group",children:[l.jsx("label",{className:"form-label",children:"Servings"}),l.jsx("input",{type:"number",step:"0.5",value:j,onChange:I=>F(I.target.value)})]}),l.jsxs("div",{className:"form-group",children:[l.jsx("label",{className:"form-label",children:"Protein (g)"}),l.jsx("input",{type:"number",value:z,onChange:I=>v(I.target.value)})]}),l.jsxs("div",{className:"form-group",children:[l.jsx("label",{className:"form-label",children:"Carbs (g)"}),l.jsx("input",{type:"number",value:L,onChange:I=>A(I.target.value)})]}),l.jsxs("div",{className:"form-group",children:[l.jsx("label",{className:"form-label",children:"Fat (g)"}),l.jsx("input",{type:"number",value:R,onChange:I=>N(I.target.value)})]})]}),l.jsx("button",{type:"submit",className:"btn-primary btn-full",disabled:oe,children:oe?"Saving...":"Log Food"})]})}),xe&&l.jsxs("div",{className:"card",style:{marginTop:"8px"},children:[l.jsxs("div",{style:{fontFamily:"var(--font-display)",fontWeight:800,marginBottom:"10px"},children:["Today: ",Math.round(xe.total_calories)," cal"]}),Ec.map(I=>{const V=xe.meals[I]||[];return V.length===0?null:l.jsxs("div",{className:"meal-section",children:[l.jsx("div",{className:"meal-title",children:I}),V.map(w=>l.jsxs("div",{style:{display:"flex",justifyContent:"space-between",padding:"4px 0",fontSize:"0.9rem"},children:[l.jsx("span",{children:w.food_name}),l.jsx("span",{style:{color:"var(--text-3)"},children:w.calories?`${Math.round(w.calories)} cal`:""})]},w.id))]},I)})]})]})}const Pc=[{label:"16:8 Daily",hours:16,type:"daily"},{label:"18:6",hours:18,type:"daily"},{label:"20:4",hours:20,type:"daily"},{label:"24h Weekly",hours:24,type:"weekly_24"},{label:"48h",hours:48,type:"long_48"},{label:"72h Kickoff",hours:72,type:"long_72"}];function Gl({label:o,value:c,target:s,unit:d,color:p,inverse:y}){const g=s>0?Math.min(100,Math.round(c/s*100)):0,P=y?c<=s:c>=s*.85;return l.jsxs("div",{style:{marginBottom:"14px"},children:[l.jsxs("div",{style:{display:"flex",justifyContent:"space-between",marginBottom:"4px",fontSize:"0.85rem"},children:[l.jsx("span",{style:{fontWeight:700},children:o}),l.jsxs("span",{style:{color:P?"var(--green-dark)":"var(--text-3)",fontWeight:600},children:[Math.round(c),y?"":` / ${s}`," ",d,y&&` / ${s} cap`]})]}),l.jsx("div",{className:"progress-bar",style:{height:"8px"},children:l.jsx("div",{className:"progress-bar-fill",style:{width:`${g}%`,background:y?c<=s?"var(--green)":"var(--red)":p}})})]})}function vm({fast:o}){const[,c]=m.useState(0);m.useEffect(()=>{const P=setInterval(()=>c(S=>S+1),6e4);return()=>clearInterval(P)},[]);const s=(new Date-new Date(o.started_at))/1e3/3600,d=Math.min(100,s/o.target_hours*100),p=s>=o.target_hours,y=Math.floor(s),g=Math.floor((s-y)*60);return l.jsxs("div",{children:[l.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"baseline",marginBottom:"8px"},children:[l.jsxs("span",{style:{fontFamily:"var(--font-display)",fontWeight:900,fontSize:"1.6rem"},children:[y,"h ",g,"m"]}),l.jsxs("span",{style:{fontSize:"0.85rem",color:"var(--text-3)",fontWeight:600},children:["/ ",o.target_hours,"h target"]})]}),l.jsx("div",{className:"progress-bar",style:{height:"12px"},children:l.jsx("div",{className:"progress-bar-fill",style:{width:`${d}%`,background:p?"linear-gradient(90deg, #00C853, #69F0AE)":"linear-gradient(90deg, #7C4DFF, #B388FF)"}})}),p&&l.jsx("div",{style:{marginTop:"8px",fontSize:"0.9rem",color:"var(--green-dark)",fontWeight:700},children:"✓ Target reached — you can end now or push further"})]})}function ym(){const[o,c]=m.useState(null),[s,d]=m.useState(null),[p,y]=m.useState(!0),[g,P]=m.useState(Pc[0]),[S,T]=m.useState(!1),z=Ge();m.useEffect(()=>{v()},[]);async function v(){try{const[W,B]=await Promise.all([J.nutritionToday(),J.getElectrolytesToday()]);c(W),d(B)}catch(W){console.error(W)}finally{y(!1)}}async function L(){T(!0);try{await J.startFast({target_hours:g.hours,fast_type:g.type}),await v()}catch(W){alert(W.message)}finally{T(!1)}}async function A(){if(confirm("End this fast now?")){T(!0);try{const W=await J.endFast(o.active_fast.id);alert(`Fast ended. +${W.points_awarded} pts earned.`),await v()}catch(W){alert(W.message)}finally{T(!1)}}}async function R(W){const B={morning:{sodium_mg:2300,potassium_mg:800,magnesium_mg:0,notes:"Morning drink"},midday:{sodium_mg:2300,potassium_mg:800,magnesium_mg:0,notes:"Midday drink"},mag_pm:{sodium_mg:0,potassium_mg:0,magnesium_mg:400,notes:"Magnesium glycinate PM"}};try{await J.logElectrolytes(B[W]),await v()}catch(Z){alert(Z.message)}}if(p)return l.jsx("div",{className:"page",children:l.jsx("div",{className:"loading",children:"Loading..."})});if(!o)return l.jsx("div",{className:"page",children:l.jsx("div",{className:"error-msg",children:"Failed to load"})});const N=o.macros,j=o.targets,F=o.compliance,_=o.eating_window;return l.jsxs("div",{className:"page",children:[l.jsx("h1",{className:"page-title",children:"Nutrition"}),l.jsxs("div",{className:`gate-banner ${F.compliant_day?"gate-earned":"gate-locked"}`,children:[l.jsx("div",{className:"section-label",style:{marginBottom:"4px"},children:"Today's Keto Day"}),l.jsx("div",{style:{fontFamily:"var(--font-display)",fontWeight:900,fontSize:"1.4rem",color:F.compliant_day?"var(--green)":"var(--accent)"},children:F.compliant_day?"✓ Compliant":"⏳ In progress"}),l.jsxs("div",{style:{fontSize:"0.82rem",color:"var(--text-2)",marginTop:"6px"},children:[F.carb_ok?"✓":"✗"," Net carbs under cap  · ",F.protein_ok?"✓":"✗"," Protein target"]})]}),l.jsxs("div",{className:"card",children:[l.jsx("div",{className:"section-label",children:"Fasting"}),o.active_fast?l.jsxs("div",{children:[l.jsxs("div",{style:{fontSize:"0.8rem",color:"var(--text-3)",marginBottom:"8px"},children:[o.active_fast.fast_type.replace("_"," ")," fast in progress"]}),l.jsx(vm,{fast:o.active_fast}),l.jsx("button",{className:"btn-outline btn-full",onClick:A,disabled:S,style:{marginTop:"14px"},children:"End Fast"})]}):l.jsxs("div",{children:[l.jsxs("div",{style:{marginBottom:"10px",fontSize:"0.85rem",color:"var(--text-2)"},children:["Eating window: ",l.jsx("strong",{children:_.display}),_.in_window_now?l.jsx("span",{style:{color:"var(--green-dark)",marginLeft:"8px"},children:"● open"}):l.jsx("span",{style:{color:"var(--text-3)",marginLeft:"8px"},children:"○ closed"})]}),l.jsx("div",{style:{display:"grid",gridTemplateColumns:"repeat(3, 1fr)",gap:"6px",marginBottom:"12px"},children:Pc.map(W=>l.jsx("button",{className:g.hours===W.hours?"btn-primary btn-sm":"btn-outline btn-sm",onClick:()=>P(W),children:W.label},W.hours))}),l.jsxs("button",{className:"btn-primary btn-full",onClick:L,disabled:S,children:["Start ",g.label," Fast"]})]})]}),l.jsxs("div",{className:"card",children:[l.jsx("div",{className:"section-label",children:"Today's Macros"}),l.jsx(Gl,{label:"Net carbs",value:N.net_carbs_g,target:j.net_carbs_cap,unit:"g",inverse:!0}),l.jsx(Gl,{label:"Protein",value:N.protein_g,target:j.protein_g,unit:"g",color:"var(--accent)"}),l.jsx(Gl,{label:"Fat",value:N.fat_g,target:j.fat_g,unit:"g",color:"#FFA726"}),l.jsx(Gl,{label:"Calories",value:N.calories,target:j.calories,unit:"kcal",color:"#2979FF"}),l.jsx("button",{className:"btn-outline btn-full btn-sm",onClick:()=>z("/food"),style:{marginTop:"8px"},children:"+ Log Food"})]}),s&&l.jsxs("div",{className:"card",children:[l.jsx("div",{className:"section-label",children:"Electrolytes Today"}),l.jsxs("div",{style:{display:"grid",gridTemplateColumns:"repeat(3, 1fr)",gap:"10px",marginBottom:"12px"},children:[l.jsxs("div",{style:{textAlign:"center"},children:[l.jsx("div",{style:{fontSize:"0.72rem",color:"var(--text-3)",fontWeight:700},children:"SODIUM"}),l.jsx("div",{style:{fontFamily:"var(--font-display)",fontWeight:900,fontSize:"1.1rem"},children:s.sodium_mg}),l.jsxs("div",{style:{fontSize:"0.7rem",color:"var(--text-3)"},children:["/ ",s.targets.sodium_mg,"mg"]})]}),l.jsxs("div",{style:{textAlign:"center"},children:[l.jsx("div",{style:{fontSize:"0.72rem",color:"var(--text-3)",fontWeight:700},children:"POTASSIUM"}),l.jsx("div",{style:{fontFamily:"var(--font-display)",fontWeight:900,fontSize:"1.1rem"},children:s.potassium_mg}),l.jsxs("div",{style:{fontSize:"0.7rem",color:"var(--text-3)"},children:["/ ",s.targets.potassium_mg,"mg"]})]}),l.jsxs("div",{style:{textAlign:"center"},children:[l.jsx("div",{style:{fontSize:"0.72rem",color:"var(--text-3)",fontWeight:700},children:"MAGNESIUM"}),l.jsx("div",{style:{fontFamily:"var(--font-display)",fontWeight:900,fontSize:"1.1rem"},children:s.magnesium_mg}),l.jsxs("div",{style:{fontSize:"0.7rem",color:"var(--text-3)"},children:["/ ",s.targets.magnesium_mg,"mg"]})]})]}),l.jsxs("div",{style:{display:"grid",gridTemplateColumns:"repeat(3, 1fr)",gap:"6px"},children:[l.jsx("button",{className:"btn-outline btn-sm",onClick:()=>R("morning"),children:"+ Morning"}),l.jsx("button",{className:"btn-outline btn-sm",onClick:()=>R("midday"),children:"+ Midday"}),l.jsx("button",{className:"btn-outline btn-sm",onClick:()=>R("mag_pm"),children:"+ Mag PM"})]})]})]})}function xm(){const[o,c]=m.useState([]),[s,d]=m.useState(""),[p,y]=m.useState("100"),[g,P]=m.useState(null),[S,T]=m.useState(!0);m.useEffect(()=>{z()},[]);async function z(){try{const[A,R]=await Promise.all([J.listRewards(),J.today()]);c(A),P(R)}catch(A){console.error(A)}finally{T(!1)}}async function v(A){if(A.preventDefault(),!!s.trim())try{await J.createReward({name:s,point_cost:parseInt(p)||100}),d(""),y("100"),await z()}catch(R){alert(R.message)}}async function L(A){try{await J.redeemReward(A),await z()}catch(R){alert(R.message)}}return S?l.jsx("div",{className:"page",children:l.jsx("div",{className:"loading",children:"Loading..."})}):l.jsxs("div",{className:"page",children:[l.jsx("h1",{className:"page-title",children:"🎮 Rewards"}),g&&l.jsxs("div",{className:`gate-banner ${g.gate_passed?"gate-earned":"gate-locked"}`,style:{padding:"18px",marginBottom:"14px"},children:[l.jsx("div",{style:{fontWeight:800,fontSize:"1rem",fontFamily:"var(--font-display)"},children:g.gate_passed?l.jsx("span",{style:{color:"var(--green-dark)"},children:"✨ Rewards Unlocked"}):l.jsxs("span",{style:{color:"var(--text-2)"},children:["🔒 Earn ",g.points_remaining," more pts"]})}),l.jsxs("div",{style:{fontSize:"0.82rem",color:"var(--text-3)",fontWeight:600,marginTop:"2px"},children:[g.points_earned," / ",g.daily_minimum," pts today"]})]}),l.jsxs("div",{className:"card",children:[l.jsx("div",{className:"section-label",children:"Your Rewards"}),o.length===0&&l.jsx("div",{style:{textAlign:"center",color:"var(--text-3)",padding:"20px",fontWeight:500},children:"No rewards yet — add one below!"}),o.map(A=>l.jsxs("div",{className:"reward-card",children:[l.jsxs("div",{children:[l.jsx("div",{style:{fontWeight:700,fontSize:"0.95rem"},children:A.name}),l.jsxs("div",{style:{fontSize:"0.78rem",color:"var(--text-3)",fontWeight:600},children:[A.point_cost," pts"]})]}),l.jsx("button",{className:g!=null&&g.gate_passed?"btn-success btn-sm":"btn-outline btn-sm",disabled:!(g!=null&&g.gate_passed),onClick:()=>L(A.id),style:{opacity:g!=null&&g.gate_passed?1:.4},children:g!=null&&g.gate_passed?"Redeem":"Locked"})]},A.id))]}),l.jsxs("div",{className:"card",children:[l.jsx("div",{className:"section-label",children:"Add New Reward"}),l.jsxs("form",{onSubmit:v,children:[l.jsxs("div",{style:{display:"grid",gridTemplateColumns:"2fr 1fr",gap:"10px",marginBottom:"10px"},children:[l.jsx("input",{value:s,onChange:A=>d(A.target.value),placeholder:"e.g. 1hr gaming",required:!0}),l.jsx("input",{type:"number",value:p,onChange:A=>y(A.target.value),placeholder:"pts"})]}),l.jsx("button",{type:"submit",className:"btn-primary btn-full btn-sm",children:"Add Reward"})]})]})]})}function Sm(){const[o,c]=m.useState(null),[s,d]=m.useState([]),[p,y]=m.useState(null),[g,P]=m.useState(null),[S,T]=m.useState(!0),z=Ge();m.useEffect(()=>{v()},[]);async function v(){try{const[j,F,_,W]=await Promise.all([J.me(),J.getRules(),J.getTargets(),J.integrationStatus()]);c(j),d(F),y(_),P(W)}catch(j){console.error(j)}finally{T(!1)}}async function L(j,F){try{await J.updateRule(j,{points:parseInt(F)})}catch(_){alert(_.message)}}async function A(j,F){const _={[j]:parseInt(F)};try{await J.updateTargets(_),y({...p,..._})}catch(W){alert(W.message)}}async function R(j){try{const F=await(j==="strava"?J.stravaAuth:J.polarAuth)();window.location.href=F.auth_url}catch(F){alert(F.message)}}async function N(j){if(confirm(`Disconnect ${j}?`))try{await J.disconnect(j),await v()}catch(F){alert(F.message)}}return S?l.jsx("div",{className:"page",children:l.jsx("div",{className:"loading",children:"Loading..."})}):l.jsxs("div",{className:"page",children:[l.jsx("h1",{className:"page-title",children:"Settings"}),o&&l.jsxs("div",{className:"card",style:{display:"flex",alignItems:"center",gap:"14px"},children:[l.jsx("div",{style:{width:"48px",height:"48px",borderRadius:"14px",background:"linear-gradient(135deg, var(--accent), var(--accent-light))",display:"flex",alignItems:"center",justifyContent:"center",color:"#fff",fontFamily:"var(--font-display)",fontWeight:900,fontSize:"1.2rem"},children:o.display_name.charAt(0).toUpperCase()}),l.jsxs("div",{children:[l.jsx("div",{style:{fontWeight:700,fontSize:"1rem"},children:o.display_name}),l.jsxs("div",{style:{fontSize:"0.82rem",color:"var(--text-3)",fontWeight:500},children:["@",o.username]})]})]}),l.jsxs("div",{className:"card",children:[l.jsx("div",{className:"section-label",children:"Features"}),l.jsxs("div",{onClick:()=>z("/meal-plan"),style:{display:"flex",alignItems:"center",justifyContent:"space-between",padding:"14px 0",borderBottom:"1px solid var(--divider)",cursor:"pointer"},children:[l.jsxs("div",{style:{display:"flex",alignItems:"center",gap:12},children:[l.jsx("span",{style:{fontSize:"1.2rem"},children:"🍽️"}),l.jsxs("div",{children:[l.jsx("div",{style:{fontWeight:700,fontSize:"0.95rem"},children:"Meal Plans"}),l.jsx("div",{style:{fontSize:"0.78rem",color:"var(--text-3)",fontWeight:500},children:"AI-generated plans with compliance tracking"})]})]}),l.jsx("span",{style:{color:"var(--text-3)",fontSize:"1.1rem"},children:"›"})]}),l.jsxs("div",{onClick:()=>z("/settings/integrations"),style:{display:"flex",alignItems:"center",justifyContent:"space-between",padding:"14px 0",borderBottom:"1px solid var(--divider)",cursor:"pointer"},children:[l.jsxs("div",{style:{display:"flex",alignItems:"center",gap:12},children:[l.jsx("span",{style:{fontSize:"1.2rem"},children:"🔗"}),l.jsxs("div",{children:[l.jsx("div",{style:{fontWeight:700,fontSize:"0.95rem"},children:"All Integrations"}),l.jsx("div",{style:{fontSize:"0.78rem",color:"var(--text-3)",fontWeight:500},children:"Strava, Polar, Garmin, Fitbit, USDA, and more"})]})]}),l.jsx("span",{style:{color:"var(--text-3)",fontSize:"1.1rem"},children:"›"})]}),l.jsxs("div",{onClick:()=>z("/rewards"),style:{display:"flex",alignItems:"center",justifyContent:"space-between",padding:"14px 0",borderBottom:"1px solid var(--divider)",cursor:"pointer"},children:[l.jsxs("div",{style:{display:"flex",alignItems:"center",gap:12},children:[l.jsx("span",{style:{fontSize:"1.2rem"},children:"🎮"}),l.jsxs("div",{children:[l.jsx("div",{style:{fontWeight:700,fontSize:"0.95rem"},children:"Rewards"}),l.jsx("div",{style:{fontSize:"0.78rem",color:"var(--text-3)",fontWeight:500},children:"Configure and redeem your rewards"})]})]}),l.jsx("span",{style:{color:"var(--text-3)",fontSize:"1.1rem"},children:"›"})]}),l.jsxs("div",{onClick:()=>z("/week"),style:{display:"flex",alignItems:"center",justifyContent:"space-between",padding:"14px 0",cursor:"pointer"},children:[l.jsxs("div",{style:{display:"flex",alignItems:"center",gap:12},children:[l.jsx("span",{style:{fontSize:"1.2rem"},children:"📊"}),l.jsxs("div",{children:[l.jsx("div",{style:{fontWeight:700,fontSize:"0.95rem"},children:"Week View"}),l.jsx("div",{style:{fontSize:"0.78rem",color:"var(--text-3)",fontWeight:500},children:"Weekly progress and day-by-day breakdown"})]})]}),l.jsx("span",{style:{color:"var(--text-3)",fontSize:"1.1rem"},children:"›"})]})]}),l.jsxs("div",{className:"card",children:[l.jsx("div",{className:"section-label",children:"Point Rules"}),s.map(j=>l.jsxs("div",{style:{display:"flex",alignItems:"center",gap:"10px",padding:"10px 0",borderBottom:"1px solid var(--divider)"},children:[l.jsx("div",{style:{flex:1,fontSize:"0.88rem",fontWeight:600},children:j.description}),l.jsx("input",{type:"number",style:{width:"64px",textAlign:"center",padding:"8px",fontWeight:700},defaultValue:j.points,onBlur:F=>L(j.id,F.target.value)}),l.jsx("span",{style:{fontSize:"0.78rem",color:"var(--text-3)",fontWeight:600},children:"pts"})]},j.id))]}),p&&l.jsxs("div",{className:"card",children:[l.jsx("div",{className:"section-label",children:"Daily Targets"}),l.jsxs("div",{className:"form-group",children:[l.jsx("label",{className:"form-label",children:"Daily minimum (unlock rewards)"}),l.jsx("input",{type:"number",defaultValue:p.daily_minimum_pts,onBlur:j=>A("daily_minimum_pts",j.target.value)})]}),l.jsxs("div",{className:"form-group",children:[l.jsx("label",{className:"form-label",children:"Weekly target"}),l.jsx("input",{type:"number",defaultValue:p.weekly_target_pts,onBlur:j=>A("weekly_target_pts",j.target.value)})]}),l.jsxs("div",{className:"form-group",children:[l.jsx("label",{className:"form-label",children:"Weekly bonus"}),l.jsx("input",{type:"number",defaultValue:p.weekly_bonus_pts,onBlur:j=>A("weekly_bonus_pts",j.target.value)})]})]}),l.jsxs("div",{className:"card",children:[l.jsx("div",{className:"section-label",children:"Integrations"}),["strava","polar"].map(j=>{const F=(g==null?void 0:g[j])||{connected:!1};return l.jsxs("div",{style:{display:"flex",alignItems:"center",justifyContent:"space-between",padding:"12px 0",borderBottom:"1px solid var(--divider)"},children:[l.jsxs("div",{children:[l.jsx("div",{style:{fontWeight:700,textTransform:"capitalize",fontSize:"0.95rem"},children:j}),l.jsx("div",{style:{fontSize:"0.78rem",color:F.connected?"var(--green-dark)":"var(--text-3)",fontWeight:600},children:F.connected?"● Connected":"Not connected"})]}),F.connected?l.jsx("button",{className:"btn-outline btn-sm",onClick:()=>N(j),children:"Disconnect"}):l.jsx("button",{className:"btn-primary btn-sm",onClick:()=>R(j),children:"Connect"})]},j)}),l.jsx("div",{onClick:()=>z("/settings/integrations"),style:{padding:"12px 0",textAlign:"center",cursor:"pointer",color:"var(--accent)",fontSize:"0.88rem",fontWeight:600},children:"Configure all 11 providers →"})]}),l.jsx("button",{className:"btn-danger btn-full",style:{marginTop:"10px"},onClick:()=>{fm(),z("/login")},children:"Sign Out"})]})}function Ft({text:o,children:c}){const[s,d]=m.useState(!1);return l.jsxs("span",{style:{position:"relative",display:"inline-flex",alignItems:"center"},children:[c,l.jsx("span",{onMouseEnter:()=>d(!0),onMouseLeave:()=>d(!1),onClick:p=>{p.stopPropagation(),d(y=>!y)},style:{display:"inline-flex",alignItems:"center",justifyContent:"center",width:"18px",height:"18px",borderRadius:"50%",marginLeft:"6px",background:"rgba(0,0,0,0.06)",color:"var(--text-3)",cursor:"help",fontSize:"0.7rem",fontWeight:800,flexShrink:0},children:"?"}),s&&l.jsx("span",{style:{position:"absolute",bottom:"calc(100% + 8px)",left:"50%",transform:"translateX(-50%)",background:"var(--text)",color:"#fff",padding:"10px 14px",borderRadius:"var(--r-sm)",fontSize:"0.78rem",lineHeight:1.45,fontWeight:500,width:"240px",boxShadow:"var(--shadow-3)",zIndex:50,pointerEvents:"none"},children:o})]})}const wm=[{value:"lose_weight",label:"Lose Weight",icon:"⚖️",tip:"Focus on caloric deficit through cardio and portion-controlled nutrition."},{value:"build_strength",label:"Build Strength",icon:"💪",tip:"Progressive overload training — gradually increasing weight or resistance."},{value:"get_active",label:"Get More Active",icon:"🏃",tip:"Building a consistent movement habit. Walking counts!"},{value:"feel_better",label:"Feel Better Overall",icon:"😊",tip:"Mind-body balance — stress relief, sleep quality, energy levels."}],jm=[{value:"precontemplation",label:"I'm not exercising and haven't thought about starting",tip:"Pre-contemplation: No intention to act. We'll start with awareness and gentle nudges."},{value:"contemplation",label:"I've been thinking about getting more active but haven't started",tip:"Contemplation: Weighing pros and cons. We'll help tip the balance toward action."},{value:"preparation",label:"I do some exercise but not consistently",tip:"Preparation: Ready to commit. We'll help you build a sustainable routine."},{value:"action",label:"I've been exercising regularly for a few months",tip:"Action: Building the habit. We'll help you stay consistent and avoid burnout."},{value:"maintenance",label:"I've been exercising regularly for 6+ months",tip:"Maintenance: Solid habit. We'll help you progress and keep things interesting."}],km=["Walking","Hiking","Running","Cycling","Swimming","Bodyweight","Weights","Yoga","Martial Arts","Dance","Team Sports","Pilates","Rowing","Jump Rope","Stretching"],_m=[{value:"bicycle",label:"🚲 Bicycle"},{value:"pool",label:"🏊 Pool"},{value:"free_weights",label:"🏋️ Free Weights"},{value:"squat_rack",label:"🦵 Squat Rack"},{value:"bench_press",label:"💺 Bench Press"},{value:"machines",label:"⚙️ Machines"},{value:"resistance_bands",label:"🔗 Resistance Bands"},{value:"pull_up_bar",label:"🔩 Pull-up Bar"},{value:"kettlebell",label:"🔔 Kettlebell"},{value:"jump_rope",label:"⏩ Jump Rope"},{value:"yoga_mat",label:"🧘 Yoga Mat"},{value:"treadmill",label:"🏃 Treadmill"},{value:"stationary_bike",label:"🚴 Stationary Bike"},{value:"rowing_machine",label:"🚣 Rowing Machine"}],Cm=[{key:"ext",label:'"People important to me say I should exercise"',tip:"External regulation: exercising because others push you to. Least self-determined motivation."},{key:"intro",label:'"I feel bad about myself when I skip exercise"',tip:"Introjected regulation: guilt or obligation as a driver. Better than external, but still fragile."},{key:"ident",label:'"I value what exercise does for my health"',tip:"Identified regulation: you see exercise as personally important. A strong, durable motivator."},{key:"intr",label:'"I find exercise enjoyable and satisfying"',tip:"Intrinsic motivation: you exercise because it's fun. The most sustainable form of motivation."},{key:"amot",label:`"I don't really see why I should bother"`,tip:"Amotivation: no perceived reason to exercise. If this is high, we'll start with very small wins."}];function Nm(){const[o,c]=m.useState(0),[s,d]=m.useState(""),[p,y]=m.useState(""),[g,P]=m.useState(""),[S,T]=m.useState(""),[z,v]=m.useState(""),[L,A]=m.useState(""),[R,N]=m.useState({heart:!1,joints:!1,meds:!1}),[j,F]=m.useState({ext:0,intro:0,ident:0,intr:0,amot:0}),[_,W]=m.useState([]),[B,Z]=m.useState([]),[fe,we]=m.useState(3),[xe,re]=m.useState(30),[oe,pe]=m.useState([{name:"",cost:100}]),[Ne,ve]=m.useState([]),[Te,Me]=m.useState(!1),We=Ge(),Se=8;function H({label:b,value:ee,onChange:ie,tip:de}){return l.jsxs("div",{style:{marginBottom:"16px"},children:[l.jsx("div",{style:{fontSize:"0.9rem",marginBottom:"6px"},children:de?l.jsx(Ft,{text:de,children:b}):b}),l.jsx("div",{className:"likert-row",children:[1,2,3,4,5].map(ye=>l.jsx("button",{type:"button",className:`likert-btn ${ee===ye?"selected":""}`,onClick:()=>ie(ye),children:ye},ye))}),l.jsxs("div",{className:"likert-labels",children:[l.jsx("span",{children:"Not at all"}),l.jsx("span",{children:"Very true"})]})]})}async function I(){Me(!0);try{await J.savePhase1({primary_goal:s,ttm_stage:p}),c(2)}catch(b){alert(b.message)}finally{Me(!1)}}async function V(){Me(!0);try{let b="none";const ee=["squat_rack","bench_press","machines"],ie=["free_weights","resistance_bands","pull_up_bar","kettlebell","yoga_mat","jump_rope"];B.some(ye=>ee.includes(ye))?b="full_gym":B.some(ye=>ie.includes(ye))&&(b="basic_home"),await J.savePhase2({age:g?parseInt(g):null,height_cm:S?parseFloat(S):null,weight_kg:z?parseFloat(z):null,gender:L||null,parq_heart_condition:R.heart,parq_joint_issues:R.joints,parq_medications:R.meds,motivation_external:j.ext||null,motivation_introjected:j.intro||null,motivation_identified:j.ident||null,motivation_intrinsic:j.intr||null,motivation_amotivation:j.amot||null,activity_preferences:_.map(ye=>ye.toLowerCase().replace(/ /g,"_")),equipment_access:b,equipment_list:B,days_per_week:fe,minutes_per_session:xe});for(const ye of oe)ye.name.trim()&&await J.createReward({name:ye.name,point_cost:ye.cost||100});const de=await J.getRecommendations();ve(de.recommendations||[]),c(7)}catch(b){alert(b.message)}finally{Me(!1)}}async function w(b){try{const ee=new Date,ie=new Date(ee);ie.setDate(ee.getDate()+((8-ee.getDay())%7||7));const de=ie.toISOString().split("T")[0];await J.createProgram({name:b.name,source:b.source,source_url:b.url,start_date:de,duration_days:b.duration_days||90}),We("/")}catch(ee){alert(ee.message)}}function O(b){W(ee=>ee.includes(b)?ee.filter(ie=>ie!==b):[...ee,b])}function ae(b){Z(ee=>ee.includes(b)?ee.filter(ie=>ie!==b):[...ee,b])}const ue=Math.round((o+1)/Se*100),se={fontFamily:"var(--font-display)",fontWeight:900,letterSpacing:"-0.02em",marginBottom:"10px",fontSize:"1.4rem"};return l.jsxs("div",{className:"page",style:{paddingTop:"40px"},children:[l.jsx("div",{className:"progress-bar",style:{marginBottom:"24px"},children:l.jsx("div",{className:"progress-bar-fill",style:{width:`${ue}%`,background:"var(--accent)"}})}),o===0&&l.jsxs("div",{children:[l.jsx("h2",{style:se,children:l.jsx(Ft,{text:"Your primary goal shapes which programs we recommend, how we structure your points, and what success looks like for you.",children:"What matters most to you?"})}),l.jsx("p",{style:{color:"var(--text-3)",marginBottom:"20px",fontSize:"0.9rem"},children:"Pick one — you can change this anytime."}),l.jsx("div",{className:"option-grid",children:wm.map(b=>l.jsxs("div",{className:`option-btn ${s===b.value?"selected":""}`,onClick:()=>d(b.value),children:[l.jsx("div",{style:{fontSize:"1.5rem",marginBottom:"6px"},children:b.icon}),l.jsx(Ft,{text:b.tip,children:b.label})]},b.value))}),l.jsx("button",{className:"btn-primary btn-full",style:{marginTop:"20px"},disabled:!s,onClick:()=>c(1),children:"Continue"})]}),o===1&&l.jsxs("div",{children:[l.jsx("h2",{style:se,children:l.jsx(Ft,{text:"Based on the Transtheoretical Model (Stages of Change). This helps us match you with the right intensity — pushing too hard too early is the #1 reason people quit.",children:"Where are you now?"})}),l.jsx("p",{style:{color:"var(--text-3)",marginBottom:"20px",fontSize:"0.9rem"},children:"Be honest — there's no wrong answer."}),jm.map(b=>l.jsx("div",{className:`option-btn ${p===b.value?"selected":""}`,style:{textAlign:"left",marginBottom:"10px"},onClick:()=>y(b.value),children:l.jsx(Ft,{text:b.tip,children:b.label})},b.value)),l.jsx("button",{className:"btn-primary btn-full",style:{marginTop:"16px"},disabled:!p||Te,onClick:I,children:Te?"...":"Continue"})]}),o===2&&l.jsxs("div",{children:[l.jsx("h2",{style:se,children:l.jsx(Ft,{text:"Based on the PAR-Q+ (Physical Activity Readiness Questionnaire). A standard pre-exercise safety screening used by fitness professionals worldwide.",children:"Quick health check"})}),l.jsx("p",{style:{color:"var(--text-3)",marginBottom:"20px",fontSize:"0.9rem"},children:"For your safety — this takes 10 seconds."}),[{key:"heart",label:"I have a heart condition or high blood pressure"},{key:"joints",label:"I have bone or joint problems that could worsen with exercise"},{key:"meds",label:"I take prescription medications for a chronic condition"}].map(b=>l.jsxs("label",{style:{display:"flex",gap:"12px",padding:"12px 0",cursor:"pointer",borderBottom:"1px solid var(--divider)"},children:[l.jsx("input",{type:"checkbox",checked:R[b.key],onChange:ee=>N({...R,[b.key]:ee.target.checked}),style:{width:"auto"}}),l.jsx("span",{style:{fontSize:"0.9rem"},children:b.label})]},b.key)),(R.heart||R.joints||R.meds)&&l.jsx("div",{style:{marginTop:"12px",padding:"12px",background:"#FFF3E0",borderRadius:"var(--r-sm)",fontSize:"0.85rem",color:"#E65100"},children:"⚠️ We recommend checking with your doctor before starting. This won't stop you — just be mindful."}),l.jsx("button",{className:"btn-primary btn-full",style:{marginTop:"20px"},onClick:()=>c(3),children:"Continue"})]}),o===3&&l.jsxs("div",{children:[l.jsx("h2",{style:se,children:"About you"}),l.jsx("p",{style:{color:"var(--text-3)",marginBottom:"20px",fontSize:"0.9rem"},children:"Optional — helps estimate nutrition needs."}),l.jsxs("div",{className:"form-group",children:[l.jsx("label",{className:"form-label",children:"Age"}),l.jsx("input",{type:"number",value:g,onChange:b=>P(b.target.value),placeholder:"e.g. 35"})]}),l.jsxs("div",{style:{display:"grid",gridTemplateColumns:"1fr 1fr",gap:"12px"},children:[l.jsxs("div",{className:"form-group",children:[l.jsx("label",{className:"form-label",children:"Height (cm)"}),l.jsx("input",{type:"number",value:S,onChange:b=>T(b.target.value),placeholder:"175"})]}),l.jsxs("div",{className:"form-group",children:[l.jsx("label",{className:"form-label",children:"Weight (kg)"}),l.jsx("input",{type:"number",value:z,onChange:b=>v(b.target.value),placeholder:"80"})]})]}),l.jsxs("div",{className:"form-group",children:[l.jsx("label",{className:"form-label",children:"Gender"}),l.jsxs("select",{value:L,onChange:b=>A(b.target.value),children:[l.jsx("option",{value:"",children:"Prefer not to say"}),l.jsx("option",{value:"male",children:"Male"}),l.jsx("option",{value:"female",children:"Female"})]})]}),l.jsxs("div",{style:{display:"flex",gap:"10px"},children:[l.jsx("button",{className:"btn-outline btn-full",onClick:()=>c(4),children:"Skip"}),l.jsx("button",{className:"btn-primary btn-full",onClick:()=>c(4),children:"Continue"})]})]}),o===4&&l.jsxs("div",{children:[l.jsx("h2",{style:se,children:l.jsx(Ft,{text:"Based on BREQ-2 (Behavioural Regulation in Exercise Questionnaire). Measures your motivation type from external pressure to intrinsic enjoyment. Your Relative Autonomy Index (RAI) score helps us calibrate how much we push vs. encourage.",children:"What drives you?"})}),l.jsx("p",{style:{color:"var(--text-3)",marginBottom:"20px",fontSize:"0.9rem"},children:"Rate each honestly — helps us calibrate your experience."}),Cm.map(b=>l.jsx(H,{label:b.label,tip:b.tip,value:j[b.key],onChange:ee=>F({...j,[b.key]:ee})},b.key)),l.jsx("button",{className:"btn-primary btn-full",onClick:()=>c(5),children:"Continue"})]}),o===5&&l.jsxs("div",{children:[l.jsx("h2",{style:se,children:"What sounds fun?"}),l.jsx("p",{style:{color:"var(--text-3)",marginBottom:"16px",fontSize:"0.9rem"},children:"Pick all that interest you."}),l.jsx("div",{className:"chip-grid",style:{marginBottom:"24px"},children:km.map(b=>l.jsx("div",{className:`chip ${_.includes(b)?"selected":""}`,onClick:()=>O(b),children:b},b))}),l.jsxs("div",{className:"form-group",children:[l.jsx("label",{className:"form-label",children:l.jsx(Ft,{text:"Select everything you have access to. This helps us recommend programs that match your available equipment — no point suggesting barbell work if you don't have a rack.",children:"Equipment you have access to"})}),l.jsx("div",{className:"chip-grid",children:_m.map(b=>l.jsx("div",{className:`chip ${B.includes(b.value)?"selected":""}`,onClick:()=>ae(b.value),children:b.label},b.value))}),B.length===0&&l.jsx("p",{style:{color:"var(--text-3)",fontSize:"0.8rem",marginTop:"8px",fontStyle:"italic"},children:"No selection = bodyweight only. That's perfectly fine!"})]}),l.jsxs("div",{style:{display:"grid",gridTemplateColumns:"1fr 1fr",gap:"12px",marginTop:"16px",marginBottom:"16px"},children:[l.jsxs("div",{className:"form-group",children:[l.jsx("label",{className:"form-label",children:"Days/week"}),l.jsx("select",{value:fe,onChange:b=>we(parseInt(b.target.value)),children:[1,2,3,4,5,6,7].map(b=>l.jsx("option",{value:b,children:b},b))})]}),l.jsxs("div",{className:"form-group",children:[l.jsx("label",{className:"form-label",children:"Minutes/session"}),l.jsx("select",{value:xe,onChange:b=>re(parseInt(b.target.value)),children:[15,20,30,45,60,90].map(b=>l.jsx("option",{value:b,children:b},b))})]})]}),l.jsx("button",{className:"btn-primary btn-full",onClick:()=>c(6),children:"Continue"})]}),o===6&&l.jsxs("div",{children:[l.jsx("h2",{style:se,children:l.jsx(Ft,{text:"The reward gate is the core mechanic: you can't access your rewards until you hit your daily point minimum. This creates a behavioral contract — earn first, enjoy second.",children:"Define your rewards"})}),l.jsx("p",{style:{color:"var(--text-3)",marginBottom:"20px",fontSize:"0.9rem"},children:"What guilty pleasures do you want to earn?"}),oe.map((b,ee)=>l.jsxs("div",{style:{display:"grid",gridTemplateColumns:"2fr 1fr",gap:"10px",marginBottom:"12px"},children:[l.jsx("input",{placeholder:"e.g. 1hr gaming",value:b.name,onChange:ie=>{const de=[...oe];de[ee]={...b,name:ie.target.value},pe(de)}}),l.jsx("input",{type:"number",placeholder:"100",value:b.cost,onChange:ie=>{const de=[...oe];de[ee]={...b,cost:parseInt(ie.target.value)||0},pe(de)}})]},ee)),l.jsx("button",{className:"btn-outline btn-sm",style:{marginBottom:"20px"},onClick:()=>pe([...oe,{name:"",cost:100}]),children:"+ Add another reward"}),l.jsx("button",{className:"btn-primary btn-full",disabled:Te,onClick:V,children:Te?"Saving...":"See Recommendations"})]}),o===7&&l.jsxs("div",{children:[l.jsx("h2",{style:se,children:"Recommended for you"}),l.jsx("p",{style:{color:"var(--text-3)",marginBottom:"20px",fontSize:"0.9rem"},children:"Pick a program and commit to 90 days."}),Ne.map(b=>l.jsxs("div",{className:"card",children:[l.jsx("div",{style:{fontWeight:700,marginBottom:"4px"},children:b.name}),l.jsxs("div",{style:{fontSize:"0.85rem",color:"var(--text-3)",marginBottom:"8px"},children:[b.difficulty," • ",b.duration_days?`${b.duration_days} days`:"Ongoing"," • ",b.source]}),l.jsx("p",{style:{fontSize:"0.9rem",marginBottom:"12px"},children:b.description}),l.jsxs("div",{style:{display:"flex",gap:"8px"},children:[l.jsx("a",{href:b.url,target:"_blank",rel:"noopener noreferrer",className:"btn-outline btn-sm",style:{display:"inline-block"},children:"View ↗"}),l.jsx("button",{className:"btn-primary btn-sm",onClick:()=>w(b),children:"Select & Commit"})]})]},b.id)),l.jsx("button",{className:"btn-outline btn-full",style:{marginTop:"8px"},onClick:()=>We("/"),children:"Skip for now"})]})]})}function Em(){const[o,c]=m.useState(null),[s,d]=m.useState(!0);m.useEffect(()=>{p()},[]);async function p(){try{c(await J.week())}catch(S){console.error(S)}finally{d(!1)}}if(s)return l.jsx("div",{className:"page",children:l.jsx("div",{className:"loading",children:"Loading..."})});if(!o)return l.jsx("div",{className:"page",children:l.jsx("div",{className:"error-msg",children:"Failed to load"})});const y=["Mon","Tue","Wed","Thu","Fri","Sat","Sun"],g=o.weekly_target>0?Math.min(100,Math.round(o.total_points_earned/o.weekly_target*100)):0,P=new Date().toISOString().split("T")[0];return l.jsxs("div",{className:"page",children:[l.jsx("h1",{className:"page-title",children:"This Week"}),l.jsxs("div",{className:"card",style:{textAlign:"center",background:"var(--accent-bg)"},children:[l.jsx("div",{className:"section-label",children:"Weekly Total"}),l.jsx("div",{style:{fontFamily:"var(--font-display)",fontSize:"2.6rem",fontWeight:900,letterSpacing:"-0.03em"},children:o.total_points_earned}),l.jsxs("div",{style:{color:"var(--text-3)",fontWeight:600,fontSize:"0.88rem",marginBottom:"12px"},children:["/ ",o.weekly_target," pts"]}),l.jsx("div",{className:"progress-bar",style:{height:"12px"},children:l.jsx("div",{className:"progress-bar-fill",style:{width:`${g}%`,background:o.hit_weekly_target?"linear-gradient(90deg, #00C853, #69F0AE)":"linear-gradient(90deg, #7C4DFF, #B388FF)"}})}),o.hit_weekly_target&&l.jsxs("div",{style:{marginTop:"10px",color:"var(--green-dark)",fontWeight:700,fontSize:"0.9rem"},children:["🏆 Target hit! +",o.weekly_bonus_earned," bonus"]}),l.jsxs("div",{style:{display:"flex",justifyContent:"center",gap:"28px",marginTop:"14px"},children:[l.jsxs("div",{children:[l.jsx("div",{style:{fontFamily:"var(--font-display)",fontWeight:900,fontSize:"1.3rem"},children:o.active_days}),l.jsx("div",{style:{fontSize:"0.72rem",color:"var(--text-3)",fontWeight:600},children:"active days"})]}),l.jsxs("div",{children:[l.jsx("div",{style:{fontFamily:"var(--font-display)",fontWeight:900,fontSize:"1.3rem"},children:o.gate_passed_days}),l.jsx("div",{style:{fontSize:"0.72rem",color:"var(--text-3)",fontWeight:600},children:"rewards earned"})]})]})]}),l.jsxs("div",{className:"card",children:[l.jsx("div",{className:"section-label",children:"Daily Breakdown"}),o.daily_breakdown.map((S,T)=>{const z=S.daily_minimum>0?Math.min(100,Math.round(S.points_earned/S.daily_minimum*100)):0,v=S.date===P;return l.jsxs("div",{style:{display:"flex",alignItems:"center",gap:"10px",padding:"10px 0",borderBottom:T<6?"1px solid var(--divider)":"none"},children:[l.jsx("div",{style:{width:"38px",fontWeight:v?800:600,fontSize:"0.85rem",color:v?"var(--accent)":"var(--text)"},children:y[T]}),l.jsx("div",{style:{flex:1},children:l.jsx("div",{className:"progress-bar",style:{height:"8px"},children:l.jsx("div",{className:"progress-bar-fill",style:{width:`${z}%`,background:S.gate_passed?"var(--green)":S.points_earned>0?"var(--amber)":"transparent"}})})}),l.jsx("div",{style:{width:"46px",textAlign:"right",fontFamily:"var(--font-display)",fontWeight:800,fontSize:"0.9rem"},children:S.points_earned}),l.jsx("div",{style:{width:"22px",textAlign:"center",fontSize:"0.9rem"},children:S.gate_passed?"✅":S.points_earned>0?"🟡":"⬜"})]},S.date)})]})]})}function Pm(){const o=Ge();return l.jsxs("div",{className:"page",style:{maxWidth:600,margin:"0 auto",padding:"40px 20px",textAlign:"center"},children:[l.jsx("div",{style:{fontSize:"3rem",marginBottom:16},children:"💪"}),l.jsx("h1",{style:{fontSize:"1.8rem",fontWeight:700,marginBottom:8},children:"Earn Before You Spend"}),l.jsx("p",{style:{color:"var(--text-2)",fontSize:"1rem",lineHeight:1.6,marginBottom:32},children:"This app runs on a simple idea: you earn points by doing healthy things (workouts, logging meals, hitting step goals), and you spend points on guilty pleasures you configure yourself."}),l.jsxs("div",{style:{background:"var(--surface-2)",borderRadius:"var(--r-md)",padding:24,marginBottom:24,textAlign:"left"},children:[l.jsxs("div",{style:{display:"flex",gap:16,marginBottom:16},children:[l.jsx("span",{style:{fontSize:"1.5rem"},children:"🔒"}),l.jsxs("div",{children:[l.jsx("strong",{children:"Daily Gate"}),l.jsx("p",{style:{color:"var(--text-2)",margin:"4px 0 0",fontSize:"0.9rem"},children:"Until you earn enough points today, your rewards stay locked. Hit your target, and the gate opens."})]})]}),l.jsxs("div",{style:{display:"flex",gap:16,marginBottom:16},children:[l.jsx("span",{style:{fontSize:"1.5rem"},children:"🎮"}),l.jsxs("div",{children:[l.jsx("strong",{children:"Your Rules"}),l.jsx("p",{style:{color:"var(--text-2)",margin:"4px 0 0",fontSize:"0.9rem"},children:"You set the rewards — gaming time, takeout, screen time, whatever you want. You hold yourself accountable."})]})]}),l.jsxs("div",{style:{display:"flex",gap:16},children:[l.jsx("span",{style:{fontSize:"1.5rem"},children:"🤖"}),l.jsxs("div",{children:[l.jsx("strong",{children:"AI Agent (Optional)"}),l.jsx("p",{style:{color:"var(--text-2)",margin:"4px 0 0",fontSize:"0.9rem"},children:"Connect an AI agent to log activities by voice, get nudged when you're behind, and have a fitness companion that knows your goals."})]})]})]}),l.jsx("button",{onClick:()=>o("/register"),style:{background:"var(--accent)",color:"#fff",border:"none",borderRadius:"var(--r-sm)",padding:"14px 32px",fontSize:"1rem",fontWeight:600,cursor:"pointer",width:"100%"},children:"Get Started"}),l.jsx("p",{style:{color:"var(--text-3)",fontSize:"0.8rem",marginTop:16},children:"Points reset weekly. Each week is a fresh start."})]})}const zc={connected:"var(--green)",configured:"var(--amber)",not_configured:"var(--text-3)"},zm={connected:"Connected",configured:"Configured",not_configured:"Not configured"};function Tm(){const[o,c]=m.useState({}),[s,d]=m.useState({}),[p,y]=m.useState(!0),[g,P]=m.useState(null),[S,T]=m.useState({}),[z,v]=m.useState(!1);m.useEffect(()=>{L()},[]);async function L(){try{const[N,j]=await Promise.all([J.listProviders(),J.fullIntegrationStatus()]);c(N),d(j)}catch(N){console.error(N)}finally{y(!1)}}function A(N){var F;P(N);const j=((F=o[N])==null?void 0:F.fields)||[];T(Object.fromEntries(j.map(_=>[_,""])))}async function R(N){v(!0);try{await J.configureIntegration(N,S),P(null),await L()}catch(j){alert(`Failed: ${j.message}`)}finally{v(!1)}}return p?l.jsx("div",{className:"page",children:l.jsx("div",{className:"loading",children:"Loading..."})}):l.jsxs("div",{className:"page",style:{maxWidth:700,margin:"0 auto"},children:[l.jsx("h1",{style:{fontSize:"1.4rem",fontWeight:700,marginBottom:24},children:"Integrations"}),l.jsx("p",{style:{color:"var(--text-2)",marginBottom:24,fontSize:"0.9rem"},children:"Configure your fitness devices and services. Credentials are encrypted and stored locally — never sent to any external server."}),Object.entries(o).map(([N,j])=>l.jsxs("div",{style:{background:"var(--surface-2)",borderRadius:"var(--r-md)",padding:20,marginBottom:12,border:s[N]==="connected"?"2px solid var(--accent)":"1px solid var(--border)"},children:[l.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:8},children:[l.jsx("strong",{style:{fontSize:"1rem"},children:j.name}),l.jsx("span",{style:{fontSize:"0.75rem",padding:"3px 10px",borderRadius:20,fontWeight:600,background:zc[s[N]||"not_configured"]+"22",color:zc[s[N]||"not_configured"]},children:zm[s[N]||"not_configured"]})]}),l.jsx("p",{style:{color:"var(--text-2)",fontSize:"0.85rem",margin:"0 0 12px"},children:j.help_text}),j.help_url&&l.jsx("a",{href:j.help_url,target:"_blank",rel:"noopener noreferrer",style:{fontSize:"0.8rem",color:"var(--accent)"},children:"Developer portal →"}),g===N?l.jsxs("div",{style:{marginTop:12,padding:16,background:"var(--surface)",borderRadius:"var(--r-sm)"},children:[j.fields.map(F=>l.jsxs("div",{style:{marginBottom:10},children:[l.jsx("label",{style:{fontSize:"0.8rem",fontWeight:600,display:"block",marginBottom:4},children:F.replace(/_/g," ")}),l.jsx("input",{type:F.includes("secret")||F.includes("token")||F.includes("key")?"password":"text",value:S[F]||"",onChange:_=>T({...S,[F]:_.target.value}),style:{width:"100%",padding:"8px 12px",borderRadius:"var(--r-sm)",border:"1px solid var(--border)",fontSize:"0.9rem",boxSizing:"border-box"},placeholder:`Enter ${F.replace(/_/g," ")}`})]},F)),l.jsxs("div",{style:{display:"flex",gap:8,marginTop:8},children:[l.jsx("button",{onClick:()=>R(N),disabled:z,style:{background:"var(--accent)",color:"#fff",border:"none",padding:"8px 20px",borderRadius:"var(--r-sm)",cursor:"pointer",fontWeight:600},children:z?"Saving...":"Save"}),l.jsx("button",{onClick:()=>P(null),style:{background:"transparent",color:"var(--text-2)",border:"1px solid var(--border)",padding:"8px 20px",borderRadius:"var(--r-sm)",cursor:"pointer"},children:"Cancel"})]})]}):l.jsx("button",{onClick:()=>A(N),style:{marginTop:8,background:"transparent",color:"var(--accent)",border:"1px solid var(--accent)",padding:"6px 16px",borderRadius:"var(--r-sm)",cursor:"pointer",fontSize:"0.85rem",fontWeight:600},children:s[N]==="not_configured"?"Configure":"Reconfigure"})]},N))]})}const bm={breakfast:"🌅",lunch:"🌞",dinner:"🌙",snack:"🍎"};function Rm(){var z;const[o,c]=m.useState(null),[s,d]=m.useState([]),[p,y]=m.useState(!0),[g,P]=m.useState({});m.useEffect(()=>{S()},[]);async function S(){try{const[v,L]=await Promise.all([J.getMealPlanToday(),J.listMealPlans()]);c(v),d(L)}catch(v){console.error(v)}finally{y(!1)}}async function T(v,L){try{await J.logMealCompliance({plan_item_id:v,status:L}),P(A=>({...A,[v]:L}))}catch(A){alert(`Failed: ${A.message}`)}}return p?l.jsx("div",{className:"page",children:l.jsx("div",{className:"loading",children:"Loading..."})}):l.jsxs("div",{className:"page",style:{maxWidth:600,margin:"0 auto"},children:[l.jsx("h1",{style:{fontSize:"1.4rem",fontWeight:700,marginBottom:8},children:"Meal Plan"}),o!=null&&o.active_plan?l.jsxs(l.Fragment,{children:[l.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:20,color:"var(--text-2)",fontSize:"0.9rem"},children:[l.jsx("span",{children:o.active_plan}),l.jsxs("span",{style:{background:"var(--accent)",color:"#fff",padding:"3px 10px",borderRadius:20,fontSize:"0.75rem",fontWeight:600},children:["Day ",o.day," / ",o.duration_days]})]}),o.daily_calories&&l.jsxs("div",{style:{background:"var(--surface-2)",borderRadius:"var(--r-sm)",padding:12,marginBottom:16,textAlign:"center",fontSize:"0.85rem",color:"var(--text-2)"},children:["Target: ",o.daily_calories," cal · ",o.diet_type||"balanced"]}),((z=o.meals)==null?void 0:z.length)>0?o.meals.map(v=>{const L=g[v.id];return l.jsxs("div",{style:{background:"var(--surface-2)",borderRadius:"var(--r-md)",padding:16,marginBottom:10,borderLeft:L==="followed"?"4px solid var(--green)":L==="skipped"?"4px solid var(--red)":L==="substituted"?"4px solid var(--amber)":"4px solid transparent"},children:[l.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"flex-start"},children:[l.jsxs("div",{children:[l.jsxs("div",{style:{fontSize:"0.75rem",color:"var(--text-3)",textTransform:"uppercase",marginBottom:4},children:[bm[v.meal_type]||"🍽️"," ",v.meal_type]}),l.jsx("div",{style:{fontWeight:600,marginBottom:4},children:v.food_name}),v.description&&l.jsx("div",{style:{fontSize:"0.85rem",color:"var(--text-2)"},children:v.description})]}),v.calories&&l.jsxs("div",{style:{fontSize:"0.85rem",fontWeight:600,color:"var(--text-2)",flexShrink:0,marginLeft:12},children:[v.calories," cal"]})]}),v.protein_g&&l.jsxs("div",{style:{fontSize:"0.75rem",color:"var(--text-3)",marginTop:6},children:["P: ",v.protein_g,"g · C: ",v.carbs_g,"g · F: ",v.fat_g,"g"]}),!L&&l.jsx("div",{style:{display:"flex",gap:6,marginTop:10},children:["followed","substituted","skipped"].map(A=>l.jsxs("button",{onClick:()=>T(v.id,A),style:{background:"transparent",border:"1px solid var(--border)",padding:"4px 12px",borderRadius:"var(--r-sm)",fontSize:"0.75rem",cursor:"pointer",color:"var(--text-2)",textTransform:"capitalize"},children:[A==="followed"?"✅":A==="skipped"?"⏭️":"🔄"," ",A]},A))})]},v.id)}):l.jsx("p",{style:{color:"var(--text-2)",textAlign:"center"},children:o.message||"No meals for today."})]}):l.jsxs("div",{style:{background:"var(--surface-2)",borderRadius:"var(--r-md)",padding:32,textAlign:"center",color:"var(--text-2)"},children:[l.jsx("div",{style:{fontSize:"2rem",marginBottom:12},children:"🍽️"}),l.jsx("p",{style:{marginBottom:8},children:"No active meal plan."}),l.jsx("p",{style:{fontSize:"0.85rem"},children:'Ask your AI agent to create one: "Generate a 7-day keto meal plan for me"'})]}),s.length>0&&l.jsxs("div",{style:{marginTop:32},children:[l.jsx("h2",{style:{fontSize:"1.1rem",fontWeight:600,marginBottom:12},children:"All Plans"}),s.map(v=>l.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center",padding:"10px 0",borderBottom:"1px solid var(--border)"},children:[l.jsxs("div",{children:[l.jsx("div",{style:{fontWeight:500},children:v.name}),l.jsxs("div",{style:{fontSize:"0.8rem",color:"var(--text-3)"},children:[v.diet_type," · ",v.duration_days," days · started ",v.start_date]})]}),l.jsx("span",{style:{fontSize:"0.75rem",padding:"2px 8px",borderRadius:12,background:v.status==="active"?"var(--green)22":"var(--text-3)22",color:v.status==="active"?"var(--green)":"var(--text-3)",fontWeight:600},children:v.status})]},v.id))]})]})}const Tc={beginner:"var(--green)",intermediate:"var(--accent)",advanced:"var(--accent-light)"},Lm={strength:"🏋️",cardio:"🏃",flexibility:"🧘",hybrid:"⚡"},bc={pending:{label:"Queued",color:"var(--amber)"},crawling:{label:"Fetching...",color:"var(--accent)"},extracting:{label:"Analyzing...",color:"var(--accent-light)"},ready:{label:"Ready",color:"var(--green)"},failed:{label:"Failed",color:"var(--red)"}};function Im(){const[o,c]=m.useState(""),[s,d]=m.useState([]),[p,y]=m.useState([]),[g,P]=m.useState(!0),[S,T]=m.useState(!1),[z,v]=m.useState(null),L=Ge();m.useEffect(()=>{A()},[]);async function A(){try{const[_,W]=await Promise.all([J.searchCatalog(""),J.listPrograms()]);d(_),y(W)}catch(_){console.error(_)}finally{P(!1)}}async function R(_){if(_.preventDefault(),!!o.trim())try{const W=await J.searchCatalog(o);d(W)}catch(W){console.error(W)}}async function N(){if(o.trim()){T(!0),v(null);try{const _=await J.researchProgram(o);_.already_exists?v({type:"info",text:`"${_.name}" is already in the catalog.`}):v({type:"success",text:_.message||"Program queued for research!"}),await A()}catch(_){v({type:"error",text:_.message})}finally{T(!1)}}}async function j(_){const W=new Date().toISOString().slice(0,10);try{await J.adoptProgram(_,W),v({type:"success",text:"Program started! Check your dashboard."}),await A()}catch(B){v({type:"error",text:B.message})}}function F(_){return p.some(W=>W.status==="active"&&W.catalog_id===_)}return g?l.jsx("div",{style:{padding:"2rem",textAlign:"center",color:"var(--text-3)"},children:"Loading..."}):l.jsxs("div",{style:{padding:"1rem",paddingBottom:"96px",maxWidth:"600px",margin:"0 auto"},children:[l.jsx("h1",{style:{fontFamily:"var(--font-display)",fontSize:"1.6rem",fontWeight:900,marginBottom:"0.5rem"},children:"Programs"}),l.jsx("p",{style:{color:"var(--text-3)",fontSize:"0.85rem",marginBottom:"1.5rem"},children:"Find a program or tell us what you're doing — we'll set it up for you."}),l.jsxs("form",{onSubmit:R,style:{display:"flex",gap:"0.5rem",marginBottom:"1rem"},children:[l.jsx("input",{type:"text",value:o,onChange:_=>c(_.target.value),placeholder:'e.g. "StrongLifts 5x5" or "Couch to 5K"',style:{flex:1,padding:"12px 16px",borderRadius:"var(--r)",border:"1px solid var(--divider)",fontFamily:"var(--font)",fontSize:"0.9rem",background:"var(--card)"}}),l.jsx("button",{type:"submit",style:{background:"var(--accent)",color:"#fff",padding:"12px 18px",boxShadow:"var(--shadow-1)"},children:"Search"})]}),l.jsx("button",{onClick:N,disabled:S||!o.trim(),style:{width:"100%",background:S?"var(--text-3)":"var(--accent)",color:"#fff",padding:"14px",marginBottom:"1rem",boxShadow:S?"none":"var(--shadow-accent)",opacity:o.trim()?1:.5},children:S?"Researching...":`Research "${o||"..."}" for me`}),z&&l.jsx("div",{style:{padding:"12px 16px",borderRadius:"var(--r-sm)",marginBottom:"1rem",fontSize:"0.85rem",fontWeight:600,background:z.type==="error"?"var(--red-ghost)":z.type==="success"?"var(--green-bg)":"var(--accent-bg)",color:z.type==="error"?"var(--red)":z.type==="success"?"var(--green-dark)":"var(--accent)"},children:z.text}),p.filter(_=>_.status==="active").length>0&&l.jsxs(l.Fragment,{children:[l.jsx("h2",{style:{fontFamily:"var(--font-display)",fontSize:"1.1rem",fontWeight:800,marginBottom:"0.75rem"},children:"Your Active Programs"}),p.filter(_=>_.status==="active").map(_=>l.jsxs("div",{onClick:()=>L(`/programs/${_.id}`),style:{background:"var(--card)",borderRadius:"var(--r)",padding:"16px",marginBottom:"0.75rem",boxShadow:"var(--shadow-1)",cursor:"pointer",border:"2px solid var(--accent-glow)"},children:[l.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center"},children:[l.jsx("strong",{style:{fontFamily:"var(--font-display)",fontSize:"1rem"},children:_.name}),l.jsxs("span",{style:{fontSize:"0.8rem",color:"var(--accent)",fontWeight:700},children:["Day ",_.current_day,"/",_.total_days]})]}),l.jsx("div",{style:{marginTop:"8px",height:"6px",borderRadius:"3px",background:"var(--divider)"},children:l.jsx("div",{style:{height:"100%",borderRadius:"3px",background:"var(--accent)",width:`${Math.min(100,_.current_day/_.total_days*100)}%`,transition:"width 0.3s ease"}})})]},_.id))]}),l.jsx("h2",{style:{fontFamily:"var(--font-display)",fontSize:"1.1rem",fontWeight:800,marginTop:"1.5rem",marginBottom:"0.75rem"},children:"Program Catalog"}),s.length===0&&l.jsx("p",{style:{color:"var(--text-3)",fontSize:"0.85rem",textAlign:"center",padding:"2rem 0"},children:"No programs found. Try searching or researching a program above."}),s.map(_=>{const W=bc[_.crawl_status]||bc.pending,B=F(_.id);return l.jsxs("div",{style:{background:"var(--card)",borderRadius:"var(--r)",padding:"16px",marginBottom:"0.75rem",boxShadow:"var(--shadow-1)"},children:[l.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"flex-start"},children:[l.jsxs("div",{children:[l.jsxs("strong",{style:{fontFamily:"var(--font-display)",fontSize:"1rem"},children:[Lm[_.category]||"📋"," ",_.name]}),_.description&&l.jsx("p",{style:{color:"var(--text-2)",fontSize:"0.82rem",marginTop:"4px"},children:_.description})]}),l.jsx("span",{style:{fontSize:"0.7rem",fontWeight:700,padding:"3px 8px",borderRadius:"var(--r-full)",color:"#fff",background:W.color,whiteSpace:"nowrap"},children:W.label})]}),l.jsxs("div",{style:{display:"flex",gap:"0.5rem",flexWrap:"wrap",marginTop:"10px"},children:[_.duration_weeks&&l.jsxs("span",{style:Xl,children:[_.duration_weeks," weeks"]}),_.frequency_per_week&&l.jsxs("span",{style:Xl,children:[_.frequency_per_week,"x/week"]}),_.difficulty&&l.jsx("span",{style:{...Xl,color:Tc[_.difficulty]||"var(--text-3)",background:`${Tc[_.difficulty]||"var(--text-3)"}11`},children:_.difficulty}),(_.equipment||[]).slice(0,3).map(Z=>l.jsx("span",{style:Xl,children:Z},Z))]}),_.crawl_status==="ready"&&!B&&l.jsx("button",{onClick:()=>j(_.id),style:{marginTop:"12px",width:"100%",background:"var(--green)",color:"#fff",padding:"10px",fontSize:"0.85rem",boxShadow:"var(--shadow-green)"},children:"Start Program"}),B&&l.jsx("div",{style:{marginTop:"12px",textAlign:"center",fontSize:"0.82rem",color:"var(--green-dark)",fontWeight:700},children:"✓ Active"}),_.crawl_status==="ready"&&l.jsx("button",{onClick:()=>L(`/catalog/${_.id}`),style:{marginTop:B?"4px":"8px",width:"100%",background:"transparent",color:"var(--accent)",padding:"8px",fontSize:"0.82rem",border:"1px solid var(--divider)"},children:"View Details"})]},_.id)})]})}const Xl={fontSize:"0.72rem",fontWeight:600,padding:"3px 10px",borderRadius:"var(--r-full)",background:"var(--divider)",color:"var(--text-2)"};function Fm(){const{id:o}=pa(),c=Ge(),[s,d]=m.useState(null),[p,y]=m.useState(null),[g,P]=m.useState(!0),[S,T]=m.useState(null),[z,v]=m.useState(null),[L,A]=m.useState(null);m.useEffect(()=>{R()},[o]);async function R(){try{const[F,_]=await Promise.all([J.getProgramSchedule(o),J.getProgramProgress(o)]);d(F),y(_)}catch(F){console.error(F)}finally{P(!1)}}async function N(F){T(F);try{const _=await J.completeWorkout(o,F,{});A(_),v(null),await R()}catch(_){alert(_.message)}finally{T(null)}}if(g)return l.jsx("div",{style:{padding:"2rem",textAlign:"center",color:"var(--text-3)"},children:"Loading..."});if(!s)return l.jsx("div",{style:{padding:"2rem",textAlign:"center"},children:"Program not found"});const j=p?p.completion_pct:0;return l.jsxs("div",{style:{padding:"1rem",paddingBottom:"96px",maxWidth:"600px",margin:"0 auto"},children:[l.jsx("button",{onClick:()=>c("/programs"),style:{background:"transparent",color:"var(--text-3)",padding:"8px 0",fontSize:"0.85rem",marginBottom:"0.5rem"},children:"← Programs"}),l.jsxs("div",{style:{background:"var(--card)",borderRadius:"var(--r-lg)",padding:"20px",boxShadow:"var(--shadow-2)",marginBottom:"1.5rem"},children:[l.jsx("h1",{style:{fontFamily:"var(--font-display)",fontSize:"1.5rem",fontWeight:900},children:s.program_name}),l.jsxs("div",{style:{display:"flex",gap:"1.5rem",marginTop:"10px",fontSize:"0.82rem",color:"var(--text-2)"},children:[l.jsxs("span",{children:["Week ",s.current_week]}),l.jsxs("span",{children:[(p==null?void 0:p.completed_workouts)||0,"/",(p==null?void 0:p.total_workouts)||0," workouts"]}),l.jsxs("span",{style:{color:"var(--accent)",fontWeight:700},children:[j,"%"]})]}),l.jsx("div",{style:{marginTop:"12px",height:"8px",borderRadius:"4px",background:"var(--divider)"},children:l.jsx("div",{style:{height:"100%",borderRadius:"4px",background:j>=100?"var(--green)":"var(--accent)",width:`${Math.min(100,j)}%`,transition:"width 0.4s ease"}})}),s.progression_rules&&l.jsxs("p",{style:{marginTop:"12px",fontSize:"0.8rem",color:"var(--text-3)",lineHeight:1.5,borderTop:"1px solid var(--divider)",paddingTop:"12px"},children:["📈 ",s.progression_rules]})]}),L&&l.jsxs("div",{style:{background:"var(--green-bg)",borderRadius:"var(--r)",padding:"16px",marginBottom:"1rem",textAlign:"center"},children:[l.jsx("div",{style:{fontSize:"1.8rem",marginBottom:"4px"},children:"🎉"}),l.jsxs("strong",{style:{color:"var(--green-dark)"},children:["+",L.total_points," points!"]}),l.jsxs("div",{style:{fontSize:"0.8rem",color:"var(--text-2)",marginTop:"4px"},children:["Workout: +",L.points_earned,L.weekly_bonus>0&&` • Week bonus: +${L.weekly_bonus}`,L.completion_bonus>0&&` • Program complete: +${L.completion_bonus}!`]}),l.jsx("button",{onClick:()=>A(null),style:{marginTop:"10px",background:"transparent",color:"var(--green-dark)",fontSize:"0.8rem",padding:"6px 16px",border:"1px solid var(--green)"},children:"Dismiss"})]}),s.today_workout&&!s.today_workout.completed&&!s.today_workout.rest_day&&l.jsxs("div",{style:{background:"var(--card)",borderRadius:"var(--r)",padding:"16px",marginBottom:"1.5rem",boxShadow:"var(--shadow-2)",border:"2px solid var(--accent-glow)"},children:[l.jsx("div",{style:{fontSize:"0.72rem",fontWeight:700,color:"var(--accent)",textTransform:"uppercase",letterSpacing:"0.05em",marginBottom:"6px"},children:"Today's Workout"}),l.jsx("strong",{style:{fontFamily:"var(--font-display)",fontSize:"1.1rem"},children:s.today_workout.workout_name||`Day ${s.today_workout.day_number}`}),l.jsx("div",{style:{marginTop:"12px"},children:(s.today_workout.exercises||[]).map((F,_)=>l.jsx(Rc,{exercise:F},_))}),l.jsx("button",{onClick:()=>N(s.today_workout.id),disabled:S===s.today_workout.id,style:{marginTop:"14px",width:"100%",padding:"14px",background:S?"var(--text-3)":"var(--green)",color:"#fff",fontSize:"0.95rem",fontWeight:800,boxShadow:"var(--shadow-green)"},children:S===s.today_workout.id?"Logging...":"✓ Complete Workout — 75 pts"})]}),l.jsx("h2",{style:{fontFamily:"var(--font-display)",fontSize:"1.1rem",fontWeight:800,marginBottom:"0.75rem"},children:"Full Schedule"}),Wm(s.schedule).map(([F,_])=>l.jsxs("div",{style:{marginBottom:"1.25rem"},children:[l.jsxs("div",{style:{fontSize:"0.78rem",fontWeight:700,color:"var(--text-3)",textTransform:"uppercase",letterSpacing:"0.04em",marginBottom:"0.5rem",display:"flex",alignItems:"center",gap:"0.5rem"},children:["Week ",F,F===s.current_week&&l.jsx("span",{style:{fontSize:"0.65rem",background:"var(--accent)",color:"#fff",padding:"2px 8px",borderRadius:"var(--r-full)"},children:"Current"})]}),_.map(W=>l.jsxs("div",{style:{background:"var(--card)",borderRadius:"var(--r-sm)",padding:"12px 14px",marginBottom:"0.4rem",boxShadow:"var(--shadow-1)",opacity:W.completed?.7:1,display:"flex",justifyContent:"space-between",alignItems:"center"},children:[l.jsxs("div",{children:[l.jsx("span",{style:{fontSize:"0.85rem",fontWeight:600},children:W.rest_day?"😴 Rest Day":W.workout_name||`Day ${W.day_number}`}),!W.rest_day&&l.jsxs("span",{style:{fontSize:"0.75rem",color:"var(--text-3)",marginLeft:"8px"},children:[(W.exercises||[]).length," exercises"]})]}),l.jsx("div",{children:W.completed?l.jsx("span",{style:{color:"var(--green)",fontSize:"0.85rem",fontWeight:700},children:"✓"}):W.rest_day?null:l.jsx("button",{onClick:()=>v(W),style:{background:"var(--green-bg)",color:"var(--green-dark)",padding:"6px 12px",fontSize:"0.75rem",fontWeight:700},children:"Do it"})})]},W.id))]},F)),z&&l.jsx("div",{style:{position:"fixed",inset:0,background:"rgba(0,0,0,0.5)",display:"flex",alignItems:"flex-end",justifyContent:"center",zIndex:100},onClick:()=>v(null),children:l.jsxs("div",{style:{background:"var(--card)",borderRadius:"var(--r-lg) var(--r-lg) 0 0",padding:"24px 20px",width:"100%",maxWidth:"600px",maxHeight:"80vh",overflow:"auto"},onClick:F=>F.stopPropagation(),children:[l.jsx("h3",{style:{fontFamily:"var(--font-display)",fontSize:"1.2rem",fontWeight:800,marginBottom:"4px"},children:z.workout_name||`Week ${z.week_number}, Day ${z.day_number}`}),l.jsx("p",{style:{color:"var(--text-3)",fontSize:"0.82rem",marginBottom:"16px"},children:'Complete the exercises below and tap "Done" when finished.'}),(z.exercises||[]).map((F,_)=>l.jsx(Rc,{exercise:F,detailed:!0},_)),z.notes&&l.jsxs("p",{style:{marginTop:"12px",fontSize:"0.8rem",color:"var(--text-2)",background:"var(--accent-bg)",padding:"10px 12px",borderRadius:"var(--r-sm)"},children:["💡 ",z.notes]}),l.jsxs("div",{style:{display:"flex",gap:"0.5rem",marginTop:"20px"},children:[l.jsx("button",{onClick:()=>v(null),style:{flex:1,background:"var(--divider)",color:"var(--text-2)",padding:"14px"},children:"Cancel"}),l.jsx("button",{onClick:()=>N(z.id),disabled:S===z.id,style:{flex:2,background:"var(--green)",color:"#fff",padding:"14px",fontWeight:800,boxShadow:"var(--shadow-green)"},children:S===z.id?"Logging...":"✓ Done — 75 pts"})]})]})})]})}function Rc({exercise:o,detailed:c}){return l.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center",padding:c?"10px 0":"6px 0",borderBottom:"1px solid var(--divider)"},children:[l.jsxs("div",{children:[l.jsx("span",{style:{fontWeight:600,fontSize:c?"0.9rem":"0.82rem"},children:o.name}),c&&o.notes&&l.jsx("div",{style:{fontSize:"0.75rem",color:"var(--text-3)",marginTop:"2px"},children:o.notes}),c&&o.weight_instruction&&l.jsxs("div",{style:{fontSize:"0.75rem",color:"var(--accent)",marginTop:"2px"},children:["🏋️ ",o.weight_instruction]})]}),l.jsxs("div",{style:{textAlign:"right",fontSize:"0.8rem",color:"var(--text-2)",fontWeight:600,whiteSpace:"nowrap"},children:[o.sets&&o.reps&&`${o.sets}×${o.reps}`,o.rest_seconds&&c&&l.jsx("div",{style:{fontSize:"0.7rem",color:"var(--text-3)"},children:o.rest_seconds>=60?`${Math.floor(o.rest_seconds/60)}m rest`:`${o.rest_seconds}s rest`})]})]})}function Wm(o){const c={};for(const s of o||[]){const d=s.week_number;c[d]||(c[d]=[]),c[d].push(s)}return Object.entries(c).sort(([s],[d])=>Number(s)-Number(d))}const Lc={beginner:"var(--green)",intermediate:"var(--accent)",advanced:"var(--accent-light)"},Bm={strength:"🏋️",cardio:"🏃",flexibility:"🧘",hybrid:"⚡"},Ic={pending:{label:"Queued",color:"var(--amber)"},crawling:{label:"Fetching...",color:"var(--accent)"},extracting:{label:"Analyzing...",color:"var(--accent-light)"},ready:{label:"Ready",color:"var(--green)"},failed:{label:"Failed",color:"var(--red)"}};function Om(){const{id:o}=pa(),c=Ge(),[s,d]=m.useState(null),[p,y]=m.useState([]),[g,P]=m.useState(!0),[S,T]=m.useState(!1),[z,v]=m.useState(null);m.useEffect(()=>{L()},[o]);async function L(){P(!0);try{const[_,W]=await Promise.all([J.getCatalogProgram(o),J.listPrograms()]);d(_),y(W)}catch(_){v({type:"error",text:_.message||"Could not load program"})}finally{P(!1)}}async function A(){T(!0),v(null);const _=new Date().toISOString().slice(0,10);try{const W=await J.adoptProgram(o,_);v({type:"success",text:"Program started! Redirecting…"}),setTimeout(()=>c(`/programs/${W.id}`),800)}catch(W){v({type:"error",text:W.message}),T(!1)}}if(g)return l.jsx("div",{style:{padding:"2rem",textAlign:"center",color:"var(--text-3)"},children:"Loading..."});if(!s)return l.jsxs("div",{style:{padding:"2rem",textAlign:"center"},children:[l.jsx("p",{style:{color:"var(--text-2)",marginBottom:"1rem"},children:"Program not found."}),l.jsx("button",{onClick:()=>c("/programs"),style:{background:"var(--accent)",color:"#fff",padding:"10px 20px"},children:"← Back to Programs"})]});const R=p.some(_=>_.status==="active"&&_.catalog_id===s.id),N=p.find(_=>_.status==="active"&&_.catalog_id===s.id),j=Ic[s.crawl_status]||Ic.pending,F=Mm(s.workouts);return l.jsxs("div",{style:{padding:"1rem",paddingBottom:"96px",maxWidth:"600px",margin:"0 auto"},children:[l.jsx("button",{onClick:()=>c("/programs"),style:{background:"transparent",color:"var(--text-3)",padding:"8px 0",fontSize:"0.85rem",marginBottom:"0.5rem"},children:"← Programs"}),l.jsxs("div",{style:{background:"var(--card)",borderRadius:"var(--r-lg)",padding:"20px",boxShadow:"var(--shadow-2)",marginBottom:"1rem"},children:[l.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"flex-start",gap:"1rem"},children:[l.jsxs("h1",{style:{fontFamily:"var(--font-display)",fontSize:"1.5rem",fontWeight:900,lineHeight:1.2},children:[Bm[s.category]||"📋"," ",s.name]}),l.jsx("span",{style:{fontSize:"0.7rem",fontWeight:700,padding:"4px 10px",borderRadius:"var(--r-full)",color:"#fff",background:j.color,whiteSpace:"nowrap"},children:j.label})]}),s.description&&l.jsx("p",{style:{color:"var(--text-2)",fontSize:"0.88rem",marginTop:"12px",lineHeight:1.5},children:s.description}),l.jsxs("div",{style:{display:"flex",gap:"0.5rem",flexWrap:"wrap",marginTop:"14px"},children:[s.duration_weeks&&l.jsxs("span",{style:ql,children:[s.duration_weeks," weeks"]}),s.frequency_per_week&&l.jsxs("span",{style:ql,children:[s.frequency_per_week,"x/week"]}),s.difficulty&&l.jsx("span",{style:{...ql,color:Lc[s.difficulty]||"var(--text-3)",background:`${Lc[s.difficulty]||"var(--text-3)"}15`},children:s.difficulty}),(s.equipment||[]).map(_=>l.jsx("span",{style:ql,children:_},_))]}),s.source_url&&l.jsx("a",{href:s.source_url,target:"_blank",rel:"noopener noreferrer",style:{display:"inline-block",marginTop:"14px",fontSize:"0.78rem",color:"var(--accent)",textDecoration:"none"},children:"🔗 Original source ↗"})]}),z&&l.jsx("div",{style:{padding:"12px 16px",borderRadius:"var(--r-sm)",marginBottom:"1rem",fontSize:"0.85rem",fontWeight:600,background:z.type==="error"?"var(--red-ghost)":"var(--green-bg)",color:z.type==="error"?"var(--red)":"var(--green-dark)"},children:z.text}),s.crawl_status==="ready"&&!R&&l.jsx("button",{onClick:A,disabled:S,style:{width:"100%",background:S?"var(--text-3)":"var(--green)",color:"#fff",padding:"14px",fontSize:"0.95rem",fontWeight:800,marginBottom:"1.25rem",boxShadow:S?"none":"var(--shadow-green)"},children:S?"Starting...":"Start This Program"}),R&&N&&l.jsx("button",{onClick:()=>c(`/programs/${N.id}`),style:{width:"100%",background:"var(--accent)",color:"#fff",padding:"14px",fontSize:"0.95rem",fontWeight:800,marginBottom:"1.25rem",boxShadow:"var(--shadow-accent)"},children:"✓ Active — Open Your Program"}),s.crawl_status!=="ready"&&l.jsx("div",{style:{padding:"14px",borderRadius:"var(--r)",background:"var(--accent-bg)",color:"var(--accent)",fontSize:"0.85rem",marginBottom:"1.25rem",textAlign:"center"},children:s.crawl_status==="failed"?`Crawl failed: ${s.crawl_error||"unknown error"}`:"Program is being researched. Check back soon."}),s.progression_rules&&l.jsxs("div",{style:{background:"var(--card)",borderRadius:"var(--r)",padding:"16px",marginBottom:"1.25rem",boxShadow:"var(--shadow-1)"},children:[l.jsx("div",{style:{fontSize:"0.72rem",fontWeight:700,color:"var(--accent)",textTransform:"uppercase",letterSpacing:"0.05em",marginBottom:"8px"},children:"📈 Progression"}),l.jsx("p",{style:{fontSize:"0.85rem",color:"var(--text-2)",lineHeight:1.5},children:s.progression_rules})]}),F.length>0&&l.jsxs(l.Fragment,{children:[l.jsx("h2",{style:{fontFamily:"var(--font-display)",fontSize:"1.1rem",fontWeight:800,marginBottom:"0.75rem"},children:"Workouts"}),F.map(([_,W])=>l.jsxs("div",{style:{marginBottom:"1.25rem"},children:[l.jsxs("div",{style:{fontSize:"0.78rem",fontWeight:700,color:"var(--text-3)",textTransform:"uppercase",letterSpacing:"0.04em",marginBottom:"0.5rem"},children:["Week ",_]}),W.map(B=>l.jsxs("div",{style:{background:"var(--card)",borderRadius:"var(--r-sm)",padding:"14px",marginBottom:"0.5rem",boxShadow:"var(--shadow-1)"},children:[l.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:B.rest_day?0:"8px"},children:[l.jsx("strong",{style:{fontSize:"0.92rem",fontFamily:"var(--font-display)"},children:B.rest_day?"😴 Rest Day":B.workout_name||`Day ${B.day_number}`}),!B.rest_day&&l.jsxs("span",{style:{fontSize:"0.72rem",color:"var(--text-3)",fontWeight:600},children:[(B.exercises||[]).length," exercises"]})]}),!B.rest_day&&(B.exercises||[]).map((Z,fe)=>l.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center",padding:"6px 0",borderTop:fe===0?"1px solid var(--divider)":"none",borderBottom:fed.day_number-p.day_number);return Object.entries(c).sort(([s],[d])=>Number(s)-Number(d))}function Dm(){const[o,c]=m.useState([]),[s,d]=m.useState(!0),[p,y]=m.useState(!1),[g,P]=m.useState(""),[S,T]=m.useState(null),z=m.useRef(null),v=Ge();m.useEffect(()=>{L()},[]),m.useEffect(()=>{var N;(N=z.current)==null||N.scrollIntoView({behavior:"smooth"})},[o]);async function L(){try{const N=await J.getThread();c(N.messages||[])}catch(N){console.error(N)}finally{d(!1)}}async function A(N){N.preventDefault();const j=g.trim();if(!(!j||p)){y(!0),T(null);try{const F=await J.sendSupportMessage(j);P(""),T(F.message),await L()}catch(F){T(F.message)}finally{y(!1)}}}function R(N){const j=new Date(N),_=new Date-j;return _<6e4?"Just now":_<36e5?`${Math.floor(_/6e4)}m ago`:_<864e5?`${Math.floor(_/36e5)}h ago`:j.toLocaleDateString(void 0,{month:"short",day:"numeric"})+" "+j.toLocaleTimeString(void 0,{hour:"2-digit",minute:"2-digit"})}return s?l.jsx("div",{style:{padding:"2rem",textAlign:"center",color:"var(--text-3)"},children:"Loading..."}):l.jsxs("div",{style:{display:"flex",flexDirection:"column",height:"calc(100dvh - 64px)",maxWidth:"600px",margin:"0 auto"},children:[l.jsxs("div",{style:{padding:"16px",borderBottom:"1px solid var(--divider)",display:"flex",alignItems:"center",gap:"12px"},children:[l.jsx("button",{onClick:()=>v("/"),style:{background:"transparent",color:"var(--text-3)",padding:"4px 0",fontSize:"1.2rem"},children:"←"}),l.jsxs("div",{children:[l.jsx("h1",{style:{fontFamily:"var(--font-display)",fontSize:"1.2rem",fontWeight:800,margin:0},children:"Support"}),l.jsx("p",{style:{color:"var(--text-3)",fontSize:"0.75rem",margin:0},children:"Ask a question — we'll get back to you"})]})]}),l.jsxs("div",{style:{flex:1,overflow:"auto",padding:"16px",display:"flex",flexDirection:"column",gap:"12px"},children:[o.length===0&&l.jsxs("div",{style:{textAlign:"center",color:"var(--text-3)",fontSize:"0.85rem",marginTop:"2rem"},children:[l.jsx("div",{style:{fontSize:"2rem",marginBottom:"8px"},children:"💬"}),"No messages yet. Ask anything about your workouts, program, or the app."]}),o.map(N=>l.jsx("div",{style:{display:"flex",justifyContent:N.sender==="user"?"flex-end":"flex-start"},children:l.jsxs("div",{style:{maxWidth:"80%",padding:"10px 14px",borderRadius:N.sender==="user"?"var(--r) var(--r) 4px var(--r)":"var(--r) var(--r) var(--r) 4px",background:N.sender==="user"?"var(--accent)":"var(--card)",color:N.sender==="user"?"#fff":"var(--text)",boxShadow:"var(--shadow-1)",fontSize:"0.88rem",lineHeight:1.5},children:[l.jsx("div",{children:N.body}),l.jsxs("div",{style:{fontSize:"0.68rem",marginTop:"4px",opacity:.7,textAlign:"right"},children:[R(N.created_at),N.sender==="user"&&N.read_at&&" ✓"]})]})},N.id)),S&&l.jsx("div",{style:{textAlign:"center",fontSize:"0.8rem",color:"var(--green-dark)",padding:"8px",background:"var(--green-bg)",borderRadius:"var(--r-sm)"},children:S}),l.jsx("div",{ref:z})]}),l.jsxs("form",{onSubmit:A,style:{padding:"12px 16px",borderTop:"1px solid var(--divider)",display:"flex",gap:"8px",background:"var(--card)"},children:[l.jsx("input",{type:"text",value:g,onChange:N=>P(N.target.value),placeholder:"Type your question...",maxLength:2e3,style:{flex:1,padding:"12px 14px",borderRadius:"var(--r-full)",border:"1px solid var(--divider)",fontFamily:"var(--font)",fontSize:"0.88rem",background:"var(--bg)"}}),l.jsx("button",{type:"submit",disabled:!g.trim()||p,style:{background:p?"var(--text-3)":"var(--accent)",color:"#fff",borderRadius:"var(--r-full)",width:"44px",height:"44px",padding:0,display:"flex",alignItems:"center",justifyContent:"center",fontSize:"1.1rem",boxShadow:"var(--shadow-accent)",opacity:g.trim()?1:.5},children:"↑"})]})]})}function Fc(){const{threadId:o}=pa();return o?l.jsx($m,{threadId:o}):l.jsx(Am,{})}function Am(){const[o,c]=m.useState([]),[s,d]=m.useState(!0),[p,y]=m.useState(null),g=Ge();m.useEffect(()=>{P()},[]);async function P(){try{const S=await J.listSupportThreads();c(S)}catch(S){y(S.message)}finally{d(!1)}}return s?l.jsx("div",{style:{padding:"2rem",textAlign:"center",color:"var(--text-3)"},children:"Loading..."}):p?l.jsx("div",{style:{padding:"2rem",textAlign:"center",color:"var(--red)"},children:p}):l.jsxs("div",{style:{padding:"1rem",paddingBottom:"96px",maxWidth:"600px",margin:"0 auto"},children:[l.jsxs("div",{style:{display:"flex",alignItems:"center",gap:"12px",marginBottom:"1.5rem"},children:[l.jsx("button",{onClick:()=>g("/"),style:{background:"transparent",color:"var(--text-3)",padding:"4px 0",fontSize:"1.2rem"},children:"←"}),l.jsx("h1",{style:{fontFamily:"var(--font-display)",fontSize:"1.4rem",fontWeight:900,margin:0},children:"Support Inbox"})]}),o.length===0&&l.jsx("p",{style:{textAlign:"center",color:"var(--text-3)",fontSize:"0.85rem",marginTop:"3rem"},children:"No support conversations yet."}),o.map(S=>l.jsxs("div",{onClick:()=>g(`/support/admin/${S.id}`),style:{background:"var(--card)",borderRadius:"var(--r)",padding:"14px 16px",marginBottom:"0.6rem",boxShadow:"var(--shadow-1)",cursor:"pointer",border:S.unread_admin>0?"2px solid var(--accent-glow)":"1px solid var(--card-border)"},children:[l.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center"},children:[l.jsx("strong",{style:{fontFamily:"var(--font-display)",fontSize:"0.95rem"},children:S.user_name}),l.jsxs("div",{style:{display:"flex",alignItems:"center",gap:"8px"},children:[S.unread_admin>0&&l.jsx("span",{style:{background:"var(--accent)",color:"#fff",fontSize:"0.65rem",fontWeight:800,width:"20px",height:"20px",borderRadius:"50%",display:"flex",alignItems:"center",justifyContent:"center"},children:S.unread_admin}),l.jsxs("span",{style:{fontSize:"0.72rem",color:"var(--text-3)"},children:[S.message_count," msgs"]})]})]}),S.last_message&&l.jsxs("p",{style:{color:"var(--text-2)",fontSize:"0.82rem",marginTop:"4px",whiteSpace:"nowrap",overflow:"hidden",textOverflow:"ellipsis"},children:[l.jsx("span",{style:{color:"var(--text-3)",fontWeight:600},children:S.last_message.sender==="admin"?"You: ":""}),S.last_message.body]})]},S.id))]})}function $m({threadId:o}){const[c,s]=m.useState(null),[d,p]=m.useState(!0),[y,g]=m.useState(!1),[P,S]=m.useState(""),T=m.useRef(null),z=Ge();m.useEffect(()=>{v()},[o]),m.useEffect(()=>{var N;(N=T.current)==null||N.scrollIntoView({behavior:"smooth"})},[c]);async function v(){try{const N=await J.getAdminThread(o);s(N)}catch(N){console.error(N)}finally{p(!1)}}async function L(N){N.preventDefault();const j=P.trim();if(!(!j||y)){g(!0);try{await J.replySupportThread(o,j),S(""),await v()}catch(F){alert(F.message)}finally{g(!1)}}}function A(N){const j=new Date(N);return j.toLocaleDateString(void 0,{month:"short",day:"numeric"})+" "+j.toLocaleTimeString(void 0,{hour:"2-digit",minute:"2-digit"})}if(d)return l.jsx("div",{style:{padding:"2rem",textAlign:"center",color:"var(--text-3)"},children:"Loading..."});if(!c)return l.jsx("div",{style:{padding:"2rem",textAlign:"center"},children:"Thread not found"});const R=c.latest_context||{};return l.jsxs("div",{style:{display:"flex",flexDirection:"column",height:"calc(100dvh - 64px)",maxWidth:"600px",margin:"0 auto"},children:[l.jsxs("div",{style:{padding:"14px 16px",borderBottom:"1px solid var(--divider)"},children:[l.jsxs("div",{style:{display:"flex",alignItems:"center",gap:"12px"},children:[l.jsx("button",{onClick:()=>z("/support/admin"),style:{background:"transparent",color:"var(--text-3)",padding:"4px 0",fontSize:"1.2rem"},children:"←"}),l.jsx("h2",{style:{fontFamily:"var(--font-display)",fontSize:"1.1rem",fontWeight:800,margin:0},children:c.user_name})]}),R.program_name&&l.jsxs("div",{style:{display:"flex",gap:"0.5rem",flexWrap:"wrap",marginTop:"8px",marginLeft:"36px"},children:[l.jsx("span",{style:Fr,children:R.program_name}),R.program_day&&l.jsxs("span",{style:Fr,children:["Day ",R.program_day,"/",R.program_total_days]}),R.program_completion_pct!==void 0&&l.jsxs("span",{style:Fr,children:[R.program_completion_pct,"%"]}),l.jsxs("span",{style:{...Fr,background:R.gate_passed?"var(--green-bg)":"var(--red-ghost)",color:R.gate_passed?"var(--green-dark)":"var(--red)"},children:[R.points_today,"/",R.daily_target," pts ",R.gate_passed?"✓":"✗"]}),R.last_workout&&l.jsxs("span",{style:Fr,children:["Last: ",R.last_workout]})]})]}),l.jsxs("div",{style:{flex:1,overflow:"auto",padding:"16px",display:"flex",flexDirection:"column",gap:"12px"},children:[(c.messages||[]).map(N=>l.jsx("div",{style:{display:"flex",justifyContent:N.sender==="admin"?"flex-end":"flex-start"},children:l.jsxs("div",{style:{maxWidth:"80%",padding:"10px 14px",borderRadius:N.sender==="admin"?"var(--r) var(--r) 4px var(--r)":"var(--r) var(--r) var(--r) 4px",background:N.sender==="admin"?"var(--accent)":"var(--card)",color:N.sender==="admin"?"#fff":"var(--text)",boxShadow:"var(--shadow-1)",fontSize:"0.88rem",lineHeight:1.5},children:[l.jsx("div",{children:N.body}),l.jsxs("div",{style:{fontSize:"0.68rem",marginTop:"4px",opacity:.7,textAlign:"right"},children:[A(N.created_at),N.read_at&&" ✓"]})]})},N.id)),l.jsx("div",{ref:T})]}),l.jsxs("form",{onSubmit:L,style:{padding:"12px 16px",borderTop:"1px solid var(--divider)",display:"flex",gap:"8px",background:"var(--card)"},children:[l.jsx("input",{type:"text",value:P,onChange:N=>S(N.target.value),placeholder:"Reply...",style:{flex:1,padding:"12px 14px",borderRadius:"var(--r-full)",border:"1px solid var(--divider)",fontFamily:"var(--font)",fontSize:"0.88rem",background:"var(--bg)"}}),l.jsx("button",{type:"submit",disabled:!P.trim()||y,style:{background:y?"var(--text-3)":"var(--accent)",color:"#fff",borderRadius:"var(--r-full)",width:"44px",height:"44px",padding:0,display:"flex",alignItems:"center",justifyContent:"center",fontSize:"1.1rem",opacity:P.trim()?1:.5},children:"↑"})]})]})}const Fr={fontSize:"0.68rem",fontWeight:600,padding:"2px 8px",borderRadius:"var(--r-full)",background:"var(--divider)",color:"var(--text-2)"};function Um(){const[o,c]=m.useState([]),[s,d]=m.useState(""),[p,y]=m.useState(!1),[g,P]=m.useState(null),S=m.useRef(null);m.useEffect(()=>{J.getAIStatus().then(P).catch(()=>P({configured:!1}))},[]),m.useEffect(()=>{var v;(v=S.current)==null||v.scrollIntoView({behavior:"smooth"})},[o]);const T=async()=>{const v=s.trim();if(!v||p)return;const L={role:"user",content:v};c(A=>[...A,L]),d(""),y(!0),c(A=>[...A,{role:"assistant",content:""}]);try{const A=o.map(W=>({role:W.role,content:W.content})),R=localStorage.getItem("fitness_token"),j=(await fetch("/api/ai/chat",{method:"POST",headers:{"Content-Type":"application/json",...R?{Authorization:`Bearer ${R}`}:{}},body:JSON.stringify({message:v,history:A})})).body.getReader(),F=new TextDecoder;let _="";for(;;){const{done:W,value:B}=await j.read();if(W)break;_+=F.decode(B,{stream:!0});const Z=_.split(` +`);_=Z.pop()||"";for(const fe of Z)if(fe.startsWith("data: ")&&fe.trim()!=="data: [DONE]")try{const{text:we}=JSON.parse(fe.slice(6));we&&c(xe=>{const re=[...xe],oe=re[re.length-1];return re[re.length-1]={...oe,content:oe.content+we},re})}catch{}}}catch{c(R=>{const N=[...R];return N[N.length-1]={role:"assistant",content:"Connection error. Check that the backend is running and an AI provider is configured in Settings → Integrations."},N})}y(!1)},z=["What should I eat today?","Log my 30-minute run","How am I doing this week?","Create a meal plan for me"];return g&&!g.configured?l.jsxs("div",{className:"page",children:[l.jsx("h1",{className:"page-title",children:"AI Coach"}),l.jsxs("div",{className:"card",style:{textAlign:"center",padding:"32px 20px"},children:[l.jsx("div",{style:{fontSize:"2rem",marginBottom:"12px"},children:"🤖"}),l.jsx("h3",{style:{marginBottom:"8px"},children:"No AI provider connected"}),l.jsx("p",{style:{color:"var(--text-2)",fontSize:"0.9rem",marginBottom:"16px"},children:"Connect OpenAI, OpenRouter, Claude, Ollama, or any other provider to start chatting with your AI fitness coach."}),l.jsx("a",{href:"/settings/integrations",className:"btn-primary",style:{display:"inline-block",padding:"12px 24px",borderRadius:"var(--r)",background:"var(--accent)",color:"var(--text-inv)",textDecoration:"none",fontWeight:600,fontSize:"0.88rem"},children:"Configure AI Provider"})]})]}):l.jsxs("div",{className:"page",style:{display:"flex",flexDirection:"column",paddingBottom:"96px"},children:[l.jsxs("div",{style:{display:"flex",alignItems:"center",justifyContent:"space-between",marginBottom:"12px"},children:[l.jsx("h1",{className:"page-title",style:{marginBottom:0},children:"AI Coach"}),(g==null?void 0:g.provider)&&l.jsxs("span",{style:{fontFamily:"var(--font-mono)",fontSize:"0.7rem",color:"var(--text-3)",background:"var(--accent-bg)",padding:"4px 10px",borderRadius:"var(--r-full)"},children:[g.provider," · ",g.model]})]}),l.jsxs("div",{style:{flex:1,overflowY:"auto",marginBottom:"12px"},children:[o.length===0&&l.jsxs("div",{style:{textAlign:"center",padding:"24px 0"},children:[l.jsx("p",{style:{color:"var(--text-3)",fontSize:"0.9rem",marginBottom:"16px"},children:"Ask me anything about your fitness, nutrition, or program."}),l.jsx("div",{style:{display:"flex",flexWrap:"wrap",gap:"8px",justifyContent:"center"},children:z.map(v=>l.jsx("button",{className:"btn-outline btn-sm",onClick:()=>{d(v)},style:{fontSize:"0.8rem"},children:v},v))})]}),o.map((v,L)=>l.jsx("div",{style:{display:"flex",justifyContent:v.role==="user"?"flex-end":"flex-start",marginBottom:"10px"},children:l.jsx("div",{style:{maxWidth:"85%",padding:"10px 14px",borderRadius:v.role==="user"?"var(--r-lg) var(--r-lg) var(--r-sm) var(--r-lg)":"var(--r-lg) var(--r-lg) var(--r-lg) var(--r-sm)",background:v.role==="user"?"var(--accent)":"var(--card)",color:v.role==="user"?"var(--text-inv)":"var(--text)",border:v.role==="user"?"none":"1px solid var(--card-border)",fontSize:"0.9rem",lineHeight:"1.5",whiteSpace:"pre-wrap"},children:v.content||(p&&L===o.length-1?"...":"")})},L)),l.jsx("div",{ref:S})]}),l.jsxs("div",{style:{display:"flex",gap:"8px",position:"sticky",bottom:"72px",background:"var(--bg)",padding:"8px 0"},children:[l.jsx("input",{value:s,onChange:v=>d(v.target.value),onKeyDown:v=>v.key==="Enter"&&!v.shiftKey&&T(),placeholder:p?"Thinking...":"Ask your AI coach...",disabled:p,style:{flex:1}}),l.jsx("button",{onClick:T,disabled:p||!s.trim(),className:"btn-primary",style:{padding:"12px 18px",minWidth:"auto"},children:p?"···":"→"})]})]})}function He({children:o}){return ti()?o:l.jsx(Kp,{to:"/login"})}function Vm(){const[o,c]=m.useState(0),s=Ge(),d=xn(),p=["/login","/onboarding","/support"].some(y=>d.pathname.startsWith(y));return m.useEffect(()=>{if(p||!ti())return;J.getUnreadCount().then(g=>c(g.unread||0)).catch(()=>{});const y=setInterval(()=>{J.getUnreadCount().then(g=>c(g.unread||0)).catch(()=>{})},6e4);return()=>clearInterval(y)},[d.pathname,p]),p?null:l.jsxs("button",{onClick:()=>s("/support"),style:{position:"fixed",top:"12px",right:"12px",zIndex:90,width:"40px",height:"40px",borderRadius:"50%",background:"var(--card)",boxShadow:"var(--shadow-2)",border:"1px solid var(--divider)",display:"flex",alignItems:"center",justifyContent:"center",fontSize:"1.1rem",padding:0,cursor:"pointer"},children:["?",o>0&&l.jsx("span",{style:{position:"absolute",top:"-4px",right:"-4px",background:"var(--red)",color:"#fff",fontSize:"0.6rem",fontWeight:800,width:"18px",height:"18px",borderRadius:"50%",display:"flex",alignItems:"center",justifyContent:"center",border:"2px solid var(--bg)"},children:o})]})}function Hm(){return l.jsxs("nav",{className:"nav-bar",children:[l.jsxs(Ir,{to:"/",className:({isActive:o})=>`nav-item ${o?"active":""}`,children:[l.jsx("span",{className:"nav-icon",children:"🏠"})," Home"]}),l.jsxs(Ir,{to:"/log",className:({isActive:o})=>`nav-item ${o?"active":""}`,children:[l.jsx("span",{className:"nav-icon",children:"💪"})," Log"]}),l.jsxs(Ir,{to:"/nutrition",className:({isActive:o})=>`nav-item ${o?"active":""}`,children:[l.jsx("span",{className:"nav-icon",children:"🥑"})," Keto"]}),l.jsxs(Ir,{to:"/programs",className:({isActive:o})=>`nav-item ${o?"active":""}`,children:[l.jsx("span",{className:"nav-icon",children:"📋"})," Programs"]}),l.jsxs(Ir,{to:"/chat",className:({isActive:o})=>`nav-item ${o?"active":""}`,children:[l.jsx("span",{className:"nav-icon",children:"🧠"})," Coach"]})]})}function Qm(){return l.jsxs(l.Fragment,{children:[ti()&&l.jsx(Vm,{}),l.jsxs(Yp,{children:[l.jsx(Fe,{path:"/login",element:l.jsx(pm,{})}),l.jsx(Fe,{path:"/onboarding",element:l.jsx(He,{children:l.jsx(Nm,{})})}),l.jsx(Fe,{path:"/",element:l.jsx(He,{children:l.jsx(mm,{})})}),l.jsx(Fe,{path:"/log",element:l.jsx(He,{children:l.jsx(hm,{})})}),l.jsx(Fe,{path:"/food",element:l.jsx(He,{children:l.jsx(gm,{})})}),l.jsx(Fe,{path:"/nutrition",element:l.jsx(He,{children:l.jsx(ym,{})})}),l.jsx(Fe,{path:"/programs",element:l.jsx(He,{children:l.jsx(Im,{})})}),l.jsx(Fe,{path:"/programs/:id",element:l.jsx(He,{children:l.jsx(Fm,{})})}),l.jsx(Fe,{path:"/catalog/:id",element:l.jsx(He,{children:l.jsx(Om,{})})}),l.jsx(Fe,{path:"/rewards",element:l.jsx(He,{children:l.jsx(xm,{})})}),l.jsx(Fe,{path:"/week",element:l.jsx(He,{children:l.jsx(Em,{})})}),l.jsx(Fe,{path:"/settings",element:l.jsx(He,{children:l.jsx(Sm,{})})}),l.jsx(Fe,{path:"/support",element:l.jsx(He,{children:l.jsx(Dm,{})})}),l.jsx(Fe,{path:"/support/admin",element:l.jsx(He,{children:l.jsx(Fc,{})})}),l.jsx(Fe,{path:"/support/admin/:threadId",element:l.jsx(He,{children:l.jsx(Fc,{})})}),l.jsx(Fe,{path:"/welcome",element:l.jsx(Pm,{})}),l.jsx(Fe,{path:"/settings/integrations",element:l.jsx(He,{children:l.jsx(Tm,{})})}),l.jsx(Fe,{path:"/meal-plan",element:l.jsx(He,{children:l.jsx(Rm,{})})}),l.jsx(Fe,{path:"/chat",element:l.jsx(He,{children:l.jsx(Um,{})})})]}),ti()&&l.jsx(Hm,{})]})}lp.createRoot(document.getElementById("root")).render(l.jsx(Bc.StrictMode,{children:l.jsx(rm,{children:l.jsx(Qm,{})})})); diff --git a/diligence/frontend/index.html b/diligence/frontend/index.html new file mode 100644 index 0000000..2fe4956 --- /dev/null +++ b/diligence/frontend/index.html @@ -0,0 +1,16 @@ + + + + + + + + + Fitness Rewards + + + + +
+ + diff --git a/diligence/frontend/llms.txt b/diligence/frontend/llms.txt new file mode 100644 index 0000000..961f6ba --- /dev/null +++ b/diligence/frontend/llms.txt @@ -0,0 +1,31 @@ +# Diligence — Self-Hosted Fitness Rewards Platform + +> Data sovereignty for people. Self-hosted fitness tracker with a points-based +> behavioral economy and an MCP connector for AI agent integration. +> MIT-licensed. Deploy with a single `docker compose up -d`. + +Diligence is a self-hosted fitness web app built around a points-based reward +system. Complete fitness activities and food logging to earn points, spend +points to unlock configurable real-world rewards. Science-based onboarding +(PAR-Q+, TTM stages of change, BREQ-2 motivation assessment). + +## For AI Agents +- MCP Server: /mcp (Streamable HTTP/SSE, 14 tools) +- Agent Card: /.well-known/agent-card.json +- Skills: /.well-known/skills/diligence-fitness/SKILL.md + +## MCP Tools +- get_context, get_today, get_week (status) +- log_activity, log_food, search_food (tracking) +- get_program_schedule (programs) +- redeem_reward (rewards) +- load_meal_plan, get_meal_plan, update_meal_compliance, get_plan_progress (meal plans) +- configure_integration, get_integration_status (device sync) + +## Technical +- Source: https://github.com/diligenceworks/diligence (MIT) +- Stack: FastAPI + PostgreSQL + React/Vite + FastMCP + +## About +Built by DiligenceWorks (https://diligenceworks.online) — adversarial AI +deal-diligence platform for institutional investors. diff --git a/diligence/main.py b/diligence/main.py new file mode 100644 index 0000000..fd860d2 --- /dev/null +++ b/diligence/main.py @@ -0,0 +1,400 @@ +from __future__ import annotations + +import asyncio +import sys +import logging +from pathlib import Path +from contextlib import asynccontextmanager +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from fastapi.staticfiles import StaticFiles +from fastapi.responses import FileResponse + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger("diligence") + + +@asynccontextmanager +async def lifespan(app: FastAPI): + # Retry DB init — container may start before postgres is ready + for attempt in range(10): + try: + from diligence.database import init_db + await init_db() + logger.info("Database tables created successfully") + break + except Exception as e: + logger.warning(f"DB init attempt {attempt + 1}/10 failed: {e}") + if attempt < 9: + await asyncio.sleep(3) + else: + logger.error("Could not initialize database after 10 attempts") + + # Fail fast if SECRET_KEY not configured + from diligence.config import get_settings + _s = get_settings() + if _s.secret_key == "change-me-in-production": + logger.error("CRITICAL: SECRET_KEY not set. Run ./setup.sh or set SECRET_KEY in .env") + sys.exit(1) + + # Run lightweight migrations for schema changes + try: + await run_migrations() + logger.info("Migrations completed") + except Exception as e: + logger.warning(f"Migration failed (non-fatal): {e}") + + # Seed resources + try: + await seed_resources() + logger.info("Resource library seeded") + except Exception as e: + logger.warning(f"Resource seeding failed (non-fatal): {e}") + + # Start background crawl queue scheduler + crawl_task = None + if _s.crawl_enabled: + try: + from diligence.services.crawl_scheduler import crawl_queue_loop + crawl_task = asyncio.create_task(crawl_queue_loop()) + logger.info("Crawl queue scheduler started") + except Exception as e: + logger.warning(f"Crawl scheduler failed to start (non-fatal): {e}") + else: + logger.info("Crawl scheduler disabled (set CRAWL_ENABLED=true to enable)") + + logger.info("Diligence backend started") + yield + + if crawl_task: + crawl_task.cancel() + try: + await crawl_task + except asyncio.CancelledError: + pass + logger.info("Diligence backend shutting down") + + +app = FastAPI(title="Diligence", version="2.0.0", lifespan=lifespan) + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=False, + allow_methods=["*"], + allow_headers=["*"], +) + +# Import routers after app creation to avoid circular imports +from diligence.routers.auth import router as auth_router +from diligence.routers.onboarding import router as onboarding_router +from diligence.routers.activities import router as activities_router +from diligence.routers.food import router as food_router +from diligence.routers.points import router as points_router, rewards_router +from diligence.routers.integrations import router as integrations_router +from diligence.routers.programs import router as programs_router +from diligence.routers.catalog import router as catalog_router +from diligence.routers.support import router as support_router +from diligence.routers.nutrition import router as nutrition_router +from diligence.routers.meal_plans import router as meal_plans_router +from diligence.routers.ai_chat import router as ai_chat_router + +app.include_router(auth_router) +app.include_router(onboarding_router) +app.include_router(activities_router) +app.include_router(food_router) +app.include_router(points_router) +app.include_router(rewards_router) +app.include_router(integrations_router) +app.include_router(catalog_router) +app.include_router(support_router) +app.include_router(programs_router) +app.include_router(nutrition_router) +app.include_router(meal_plans_router) +app.include_router(ai_chat_router) + + +@app.get("/api/health") +async def health(): + return {"status": "ok", "version": "2.0.0"} + + +# --- Static frontend serving (pip install path) --- +# When running without nginx, serve the pre-built React app directly. +# The frontend/ directory is bundled with the pip package. +_frontend_dir = Path(__file__).parent / "frontend" +if _frontend_dir.is_dir() and (_frontend_dir / "index.html").exists(): + # B2A discovery files + @app.get("/llms.txt") + async def llms_txt(): + p = _frontend_dir / "llms.txt" + if p.exists(): + return FileResponse(p, media_type="text/plain") + + @app.get("/.well-known/{path:path}") + async def well_known(path: str): + p = _frontend_dir / ".well-known" / path + if p.exists(): + media = "text/plain" if path.endswith(".md") else "application/json" + return FileResponse(p, media_type=media) + + # Static assets (js, css, images) + app.mount("/assets", StaticFiles(directory=_frontend_dir / "assets"), name="assets") + + # SPA catch-all — must be last + @app.get("/{path:path}") + async def spa_catchall(path: str): + # Don't intercept /api routes (already handled above) + if path.startswith("api/"): + return + file_path = _frontend_dir / path + if file_path.is_file(): + return FileResponse(file_path) + return FileResponse(_frontend_dir / "index.html") + + logger.info(f"Serving frontend from {_frontend_dir}") + + +# --- Cross-dialect migrations --- + +async def run_migrations(): + """Add missing columns/tables. Works on both PostgreSQL and SQLite.""" + from diligence.database import engine + from diligence.config import get_settings + from sqlalchemy import text, inspect as sa_inspect + + settings = get_settings() + + async with engine.begin() as conn: + # Get table inspector + def _get_tables_and_columns(connection): + inspector = sa_inspect(connection) + tables = inspector.get_table_names() + columns = {} + for t in tables: + columns[t] = [c["name"] for c in inspector.get_columns(t)] + return tables, columns + + tables, columns = await conn.run_sync(_get_tables_and_columns) + + # v1: Add equipment_list column if missing + if "user_profiles" in tables and "equipment_list" not in columns.get("user_profiles", []): + if settings.is_sqlite: + await conn.execute(text( + "ALTER TABLE user_profiles ADD COLUMN equipment_list TEXT DEFAULT '[]'" + )) + else: + await conn.execute(text( + "ALTER TABLE user_profiles ADD COLUMN equipment_list JSONB DEFAULT '[]'" + )) + + # v2: Add catalog columns to programs table + if "programs" in tables: + prog_cols = columns.get("programs", []) + if "catalog_id" not in prog_cols: + await conn.execute(text( + "ALTER TABLE programs ADD COLUMN catalog_id VARCHAR(36)" + )) + if "current_week" not in prog_cols: + await conn.execute(text( + "ALTER TABLE programs ADD COLUMN current_week INTEGER DEFAULT 1" + )) + if "current_day" not in prog_cols: + await conn.execute(text( + "ALTER TABLE programs ADD COLUMN current_day INTEGER DEFAULT 1" + )) + + # v2.1: Add week_number to workout_logs + if "workout_logs" in tables and "week_number" not in columns.get("workout_logs", []): + await conn.execute(text( + "ALTER TABLE workout_logs ADD COLUMN week_number INTEGER DEFAULT 1" + )) + + # v4: Add is_admin to users + if "users" in tables and "is_admin" not in columns.get("users", []): + await conn.execute(text( + "ALTER TABLE users ADD COLUMN is_admin BOOLEAN DEFAULT FALSE" + )) + + # v4.1: Grant admin to first registered user if none exists + if "users" in tables and "is_admin" in columns.get("users", []): + result = await conn.execute(text( + "SELECT COUNT(*) FROM users WHERE is_admin = TRUE" + )) + admin_count = result.scalar() + if admin_count == 0: + await conn.execute(text( + "UPDATE users SET is_admin = TRUE " + "WHERE id = (SELECT id FROM users ORDER BY created_at ASC LIMIT 1)" + )) + + # v5: integration_configs table — handled by create_all + # v6: meal plan tables — handled by create_all + # v7: point rules seeding + if "point_rules" in tables and "users" in tables: + for category, points in [ + ("fast_completed", 200), + ("keto_compliant_day", 100), + ("meal_plan_followed", 40), + ("meal_plan_partial", 20), + ]: + # Check if any user is missing this rule + result = await conn.execute(text( + "SELECT u.id FROM users u " + "WHERE NOT EXISTS (" + " SELECT 1 FROM point_rules pr " + " WHERE pr.user_id = u.id AND pr.category = :cat" + ")" + ), {"cat": category}) + missing_users = result.fetchall() + for (user_id,) in missing_users: + # Use Python uuid for SQLite compatibility + import uuid + await conn.execute(text( + "INSERT INTO point_rules (id, user_id, category, points, unit, is_active) " + "VALUES (:id, :uid, :cat, :pts, 'per_event', TRUE)" + ), { + "id": str(uuid.uuid4()), + "uid": str(user_id), + "cat": category, + "pts": points, + }) + + +async def seed_resources(): + """Seed the resource library with curated fitness programs.""" + from diligence.database import async_session + from diligence.models.resource import Resource + from sqlalchemy import select + + async with async_session() as db: + existing = await db.execute(select(Resource).limit(1)) + if existing.scalar_one_or_none(): + return + + resources = [ + Resource( + name="Darebee Foundation Program", + source="darebee", + url="https://darebee.com/programs/foundation-program.html", + description="30-day beginner program. Bodyweight exercises, no equipment needed. Perfect starting point.", + goal_tags=["get_active", "feel_better"], + activity_tags=["bodyweight"], + equipment_needed="none", + ttm_stages=["precontemplation", "contemplation", "preparation"], + difficulty="beginner", + duration_days=30, + ), + Resource( + name="Darebee 30 Days of Change", + source="darebee", + url="https://darebee.com/programs/30-days-of-change.html", + description="30-day progressive program for building fitness habits. Bodyweight only.", + goal_tags=["get_active", "lose_weight", "feel_better"], + activity_tags=["bodyweight"], + equipment_needed="none", + ttm_stages=["contemplation", "preparation"], + difficulty="beginner", + duration_days=30, + ), + Resource( + name="Darebee IRONHEART", + source="darebee", + url="https://darebee.com/programs/ironheart.html", + description="Strength-focused bodyweight program. No equipment, all levels.", + goal_tags=["build_strength"], + activity_tags=["bodyweight"], + equipment_needed="none", + ttm_stages=["preparation", "action"], + difficulty="intermediate", + duration_days=30, + ), + Resource( + name="Darebee Total Body Strength", + source="darebee", + url="https://darebee.com/programs/total-body-strength.html", + description="Comprehensive strength building program using bodyweight.", + goal_tags=["build_strength"], + activity_tags=["bodyweight"], + equipment_needed="none", + ttm_stages=["action", "maintenance"], + difficulty="intermediate", + duration_days=30, + ), + Resource( + name="Darebee 8 Weeks to 5K", + source="darebee", + url="https://darebee.com/programs/8-weeks-to-5k-program.html", + description="Progressive running program from beginner to 5K in 8 weeks.", + goal_tags=["get_active", "lose_weight"], + activity_tags=["running"], + equipment_needed="none", + ttm_stages=["preparation", "action"], + difficulty="beginner", + duration_days=56, + ), + Resource( + name="StrongLifts 5x5", + source="stronglifts", + url="https://stronglifts.com/5x5/", + description="Simple barbell strength program. 3 days/week, 5 exercises, progressive overload.", + goal_tags=["build_strength"], + activity_tags=["weights"], + equipment_needed="full_gym", + ttm_stages=["preparation", "action", "maintenance"], + difficulty="beginner", + duration_days=90, + ), + Resource( + name="Darebee 30 Days of Cardio", + source="darebee", + url="https://darebee.com/programs/30-days-of-cardio.html", + description="30-day cardio program, no equipment. Great for weight loss.", + goal_tags=["lose_weight", "get_active"], + activity_tags=["bodyweight"], + equipment_needed="none", + ttm_stages=["preparation", "action"], + difficulty="beginner", + duration_days=30, + ), + Resource( + name="Darebee POWERBUILDER", + source="darebee", + url="https://darebee.com/programs/powerbuilder-program.html", + description="Advanced strength and power program using bodyweight.", + goal_tags=["build_strength"], + activity_tags=["bodyweight"], + equipment_needed="none", + ttm_stages=["action", "maintenance"], + difficulty="advanced", + duration_days=30, + ), + Resource( + name="Fitness Blender (YouTube)", + source="youtube", + url="https://www.youtube.com/@fitnessblender", + description="Free workout videos for all levels. Huge variety: HIIT, strength, yoga, pilates.", + goal_tags=["lose_weight", "build_strength", "get_active", "feel_better"], + activity_tags=["bodyweight", "yoga"], + equipment_needed="none", + ttm_stages=["contemplation", "preparation", "action", "maintenance"], + difficulty="beginner", + duration_days=None, + ), + Resource( + name="Darebee Yoga Flexibility Program", + source="darebee", + url="https://darebee.com/programs/flexibility-program.html", + description="30-day flexibility and yoga program for beginners.", + goal_tags=["feel_better"], + activity_tags=["yoga"], + equipment_needed="none", + ttm_stages=["precontemplation", "contemplation", "preparation"], + difficulty="beginner", + duration_days=30, + ), + ] + + for r in resources: + db.add(r) + await db.commit() diff --git a/diligence/models/__init__.py b/diligence/models/__init__.py new file mode 100644 index 0000000..5e08329 --- /dev/null +++ b/diligence/models/__init__.py @@ -0,0 +1,24 @@ +from diligence.models.user import User +from diligence.models.profile import UserProfile +from diligence.models.program import Program +from diligence.models.activity import ActivityLog +from diligence.models.food import FoodLog +from diligence.models.points import PointRule, DailyTarget +from diligence.models.reward import Reward, RewardRedemption +from diligence.models.oauth import OAuthToken +from diligence.models.resource import Resource +from diligence.models.catalog import ProgramCatalog, CatalogWorkout, CrawlQueue, WorkoutLog +from diligence.models.support import SupportThread, SupportMessage +from diligence.models.nutrition import NutritionGoal, Fast, ElectrolyteLog +from diligence.models.integration_config import IntegrationConfig +from diligence.models.meal_plan import MealPlan, MealPlanItem, MealCompliance + +__all__ = [ + "User", "UserProfile", "Program", "ActivityLog", "FoodLog", + "PointRule", "DailyTarget", "Reward", "RewardRedemption", + "OAuthToken", "Resource", + "ProgramCatalog", "CatalogWorkout", "CrawlQueue", "WorkoutLog", + "SupportThread", "SupportMessage", + "NutritionGoal", "Fast", "ElectrolyteLog", + "IntegrationConfig", "MealPlan", "MealPlanItem", "MealCompliance", +] diff --git a/backend/app/models/activity.py b/diligence/models/activity.py similarity index 68% rename from backend/app/models/activity.py rename to diligence/models/activity.py index 890135d..92f10f7 100644 --- a/backend/app/models/activity.py +++ b/diligence/models/activity.py @@ -2,17 +2,16 @@ from __future__ import annotations import uuid from datetime import datetime, date, timezone -from sqlalchemy import String, Integer, Text, Date, DateTime, ForeignKey, Index +from sqlalchemy import String, Integer, Text, Date, DateTime, ForeignKey, Index, Uuid, JSON from sqlalchemy.orm import Mapped, mapped_column -from sqlalchemy.dialects.postgresql import UUID, JSONB -from app.database import Base +from diligence.database import Base class ActivityLog(Base): __tablename__ = "activity_log" - id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - user_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False) + id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, default=uuid.uuid4) + user_id: Mapped[uuid.UUID] = mapped_column(Uuid, ForeignKey("users.id"), nullable=False) category: Mapped[str] = mapped_column(String(50), nullable=False) title: Mapped[str | None] = mapped_column(String(200)) description: Mapped[str | None] = mapped_column(Text) @@ -20,12 +19,12 @@ class ActivityLog(Base): source: Mapped[str] = mapped_column(String(30), default="manual") external_id: Mapped[str | None] = mapped_column(String(100)) points_earned: Mapped[int] = mapped_column(Integer, default=0) - rule_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("point_rules.id"), nullable=True) + rule_id: Mapped[uuid.UUID | None] = mapped_column(Uuid, ForeignKey("point_rules.id"), nullable=True) activity_date: Mapped[date] = mapped_column(Date, nullable=False) logged_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)) - program_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("programs.id"), nullable=True) + program_id: Mapped[uuid.UUID | None] = mapped_column(Uuid, ForeignKey("programs.id"), nullable=True) program_day: Mapped[int | None] = mapped_column(Integer) - metadata_json: Mapped[dict] = mapped_column("metadata", JSONB, default=dict) + metadata_json: Mapped[dict] = mapped_column("metadata", JSON, default=dict) __table_args__ = ( Index("idx_activity_log_user_date", "user_id", "activity_date"), diff --git a/backend/app/models/catalog.py b/diligence/models/catalog.py similarity index 76% rename from backend/app/models/catalog.py rename to diligence/models/catalog.py index aa071b8..494fdc0 100644 --- a/backend/app/models/catalog.py +++ b/diligence/models/catalog.py @@ -2,28 +2,27 @@ from __future__ import annotations import uuid from datetime import datetime, timezone -from sqlalchemy import String, Integer, Text, Boolean, DateTime, ForeignKey, UniqueConstraint +from sqlalchemy import String, Integer, Text, Boolean, DateTime, ForeignKey, UniqueConstraint, Uuid, JSON from sqlalchemy.orm import Mapped, mapped_column, relationship -from sqlalchemy.dialects.postgresql import UUID, JSONB -from app.database import Base +from diligence.database import Base class ProgramCatalog(Base): """Shared program definitions — one per program, reusable by all users.""" __tablename__ = "program_catalog" - id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, default=uuid.uuid4) name: Mapped[str] = mapped_column(String(200), nullable=False) slug: Mapped[str] = mapped_column(String(200), unique=True, nullable=False) description: Mapped[str | None] = mapped_column(Text) source_url: Mapped[str | None] = mapped_column(Text) duration_weeks: Mapped[int | None] = mapped_column(Integer) frequency_per_week: Mapped[int | None] = mapped_column(Integer) - equipment: Mapped[dict] = mapped_column(JSONB, default=list) + equipment: Mapped[dict] = mapped_column(JSON, default=list) difficulty: Mapped[str | None] = mapped_column(String(20)) category: Mapped[str | None] = mapped_column(String(50)) progression_rules: Mapped[str | None] = mapped_column(Text) - structured_data: Mapped[dict | None] = mapped_column(JSONB) + structured_data: Mapped[dict | None] = mapped_column(JSON) crawl_status: Mapped[str] = mapped_column(String(20), default="pending") crawl_error: Mapped[str | None] = mapped_column(Text) crawled_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) @@ -41,14 +40,14 @@ class CatalogWorkout(Base): """Individual workout within a catalog program.""" __tablename__ = "catalog_workouts" - id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, default=uuid.uuid4) catalog_id: Mapped[uuid.UUID] = mapped_column( - UUID(as_uuid=True), ForeignKey("program_catalog.id", ondelete="CASCADE"), nullable=False + Uuid, ForeignKey("program_catalog.id", ondelete="CASCADE"), nullable=False ) week_number: Mapped[int] = mapped_column(Integer, nullable=False) day_number: Mapped[int] = mapped_column(Integer, nullable=False) workout_name: Mapped[str | None] = mapped_column(String(200)) - exercises: Mapped[dict] = mapped_column(JSONB, nullable=False) + exercises: Mapped[dict] = mapped_column(JSON, nullable=False) notes: Mapped[str | None] = mapped_column(Text) rest_day: Mapped[bool] = mapped_column(Boolean, default=False) @@ -63,14 +62,14 @@ class CrawlQueue(Base): """Job queue for program research crawls.""" __tablename__ = "crawl_queue" - id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, default=uuid.uuid4) catalog_id: Mapped[uuid.UUID | None] = mapped_column( - UUID(as_uuid=True), ForeignKey("program_catalog.id"), nullable=True + Uuid, ForeignKey("program_catalog.id"), nullable=True ) search_query: Mapped[str] = mapped_column(String(500), nullable=False) priority: Mapped[str] = mapped_column(String(10), default="low") status: Mapped[str] = mapped_column(String(20), default="pending") - urls_to_crawl: Mapped[dict] = mapped_column(JSONB, default=list) + urls_to_crawl: Mapped[dict] = mapped_column(JSON, default=list) crawled_content: Mapped[str | None] = mapped_column(Text) error: Mapped[str | None] = mapped_column(Text) created_at: Mapped[datetime] = mapped_column( @@ -84,20 +83,20 @@ class WorkoutLog(Base): """Per-user workout completion tracking against catalog workouts.""" __tablename__ = "workout_logs" - id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, default=uuid.uuid4) user_id: Mapped[uuid.UUID] = mapped_column( - UUID(as_uuid=True), ForeignKey("users.id"), nullable=False + Uuid, ForeignKey("users.id"), nullable=False ) program_id: Mapped[uuid.UUID] = mapped_column( - UUID(as_uuid=True), ForeignKey("programs.id"), nullable=False + Uuid, ForeignKey("programs.id"), nullable=False ) catalog_workout_id: Mapped[uuid.UUID] = mapped_column( - UUID(as_uuid=True), ForeignKey("catalog_workouts.id"), nullable=False + Uuid, ForeignKey("catalog_workouts.id"), nullable=False ) week_number: Mapped[int] = mapped_column(Integer, nullable=False, default=1) completed_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=lambda: datetime.now(timezone.utc) ) - exercises_completed: Mapped[dict | None] = mapped_column(JSONB) + exercises_completed: Mapped[dict | None] = mapped_column(JSON) points_earned: Mapped[int] = mapped_column(Integer, default=0) notes: Mapped[str | None] = mapped_column(Text) diff --git a/backend/app/models/food.py b/diligence/models/food.py similarity index 80% rename from backend/app/models/food.py rename to diligence/models/food.py index 99dd4e2..432ea48 100644 --- a/backend/app/models/food.py +++ b/diligence/models/food.py @@ -3,17 +3,16 @@ from __future__ import annotations import uuid from datetime import datetime, date, timezone from decimal import Decimal -from sqlalchemy import String, Numeric, Date, DateTime, Text, ForeignKey, Index +from sqlalchemy import String, Numeric, Date, DateTime, Text, ForeignKey, Index, Uuid, JSON from sqlalchemy.orm import Mapped, mapped_column -from sqlalchemy.dialects.postgresql import UUID, JSONB -from app.database import Base +from diligence.database import Base class FoodLog(Base): __tablename__ = "food_log" - id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - user_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False) + id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, default=uuid.uuid4) + user_id: Mapped[uuid.UUID] = mapped_column(Uuid, ForeignKey("users.id"), nullable=False) meal_type: Mapped[str] = mapped_column(String(20), nullable=False) food_name: Mapped[str] = mapped_column(String(300), nullable=False) brand: Mapped[str | None] = mapped_column(String(200)) @@ -30,7 +29,7 @@ class FoodLog(Base): food_date: Mapped[date] = mapped_column(Date, nullable=False) logged_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)) off_product_id: Mapped[str | None] = mapped_column(String(50)) - off_data: Mapped[dict] = mapped_column(JSONB, default=dict) + off_data: Mapped[dict] = mapped_column(JSON, default=dict) __table_args__ = ( Index("idx_food_log_user_date", "user_id", "food_date"), diff --git a/backend/app/models/integration_config.py b/diligence/models/integration_config.py similarity index 72% rename from backend/app/models/integration_config.py rename to diligence/models/integration_config.py index 39893bf..8d23b83 100644 --- a/backend/app/models/integration_config.py +++ b/diligence/models/integration_config.py @@ -3,17 +3,16 @@ from __future__ import annotations import uuid from datetime import datetime, timezone -from sqlalchemy import String, DateTime, Text, UniqueConstraint +from sqlalchemy import String, DateTime, Text, UniqueConstraint, Uuid from sqlalchemy.orm import Mapped, mapped_column -from sqlalchemy.dialects.postgresql import UUID -from app.database import Base +from diligence.database import Base class IntegrationConfig(Base): __tablename__ = "integration_configs" - id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - user_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), nullable=False) + id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, default=uuid.uuid4) + user_id: Mapped[uuid.UUID] = mapped_column(Uuid, nullable=False) provider: Mapped[str] = mapped_column(String(50), nullable=False) config_key: Mapped[str] = mapped_column(String(100), nullable=False) config_value: Mapped[str] = mapped_column(Text, nullable=False) # Fernet encrypted diff --git a/backend/app/models/meal_plan.py b/diligence/models/meal_plan.py similarity index 72% rename from backend/app/models/meal_plan.py rename to diligence/models/meal_plan.py index 280ddd3..a5ca218 100644 --- a/backend/app/models/meal_plan.py +++ b/diligence/models/meal_plan.py @@ -4,24 +4,23 @@ from __future__ import annotations import uuid from datetime import datetime, date, timezone from decimal import Decimal -from sqlalchemy import String, Integer, Date, DateTime, Text, ForeignKey, DECIMAL +from sqlalchemy import String, Integer, Date, DateTime, Text, ForeignKey, DECIMAL, Uuid, JSON from sqlalchemy.orm import Mapped, mapped_column, relationship -from sqlalchemy.dialects.postgresql import UUID, JSONB -from app.database import Base +from diligence.database import Base class MealPlan(Base): __tablename__ = "meal_plans" - id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - user_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), nullable=False) + id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, default=uuid.uuid4) + user_id: Mapped[uuid.UUID] = mapped_column(Uuid, nullable=False) name: Mapped[str] = mapped_column(String(200), nullable=False) diet_type: Mapped[str | None] = mapped_column(String(50), nullable=True) daily_calories: Mapped[int | None] = mapped_column(Integer, nullable=True) daily_protein_g: Mapped[int | None] = mapped_column(Integer, nullable=True) daily_carbs_g: Mapped[int | None] = mapped_column(Integer, nullable=True) daily_fat_g: Mapped[int | None] = mapped_column(Integer, nullable=True) - restrictions: Mapped[dict] = mapped_column(JSONB, default=list) + restrictions: Mapped[dict] = mapped_column(JSON, default=list) duration_days: Mapped[int] = mapped_column(Integer, nullable=False) start_date: Mapped[date] = mapped_column(Date, nullable=False) status: Mapped[str] = mapped_column(String(20), default="active") @@ -33,8 +32,8 @@ class MealPlan(Base): class MealPlanItem(Base): __tablename__ = "meal_plan_items" - id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - plan_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("meal_plans.id", ondelete="CASCADE"), nullable=False) + id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, default=uuid.uuid4) + plan_id: Mapped[uuid.UUID] = mapped_column(Uuid, ForeignKey("meal_plans.id", ondelete="CASCADE"), nullable=False) day_number: Mapped[int] = mapped_column(Integer, nullable=False) meal_type: Mapped[str] = mapped_column(String(20), nullable=False) food_name: Mapped[str] = mapped_column(String(300), nullable=False) @@ -54,12 +53,12 @@ class MealPlanItem(Base): class MealCompliance(Base): __tablename__ = "meal_compliance" - id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - user_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), nullable=False) - plan_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("meal_plans.id"), nullable=False) - plan_item_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), nullable=True) + id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, default=uuid.uuid4) + user_id: Mapped[uuid.UUID] = mapped_column(Uuid, nullable=False) + plan_id: Mapped[uuid.UUID] = mapped_column(Uuid, ForeignKey("meal_plans.id"), nullable=False) + plan_item_id: Mapped[uuid.UUID | None] = mapped_column(Uuid, nullable=True) compliance_date: Mapped[date] = mapped_column(Date, nullable=False) status: Mapped[str] = mapped_column(String(20), nullable=False) # followed, substituted, skipped substitution: Mapped[str | None] = mapped_column(Text, nullable=True) - food_log_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), nullable=True) + food_log_id: Mapped[uuid.UUID | None] = mapped_column(Uuid, nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)) diff --git a/backend/app/models/nutrition.py b/diligence/models/nutrition.py similarity index 80% rename from backend/app/models/nutrition.py rename to diligence/models/nutrition.py index 205dd7e..82caf94 100644 --- a/backend/app/models/nutrition.py +++ b/diligence/models/nutrition.py @@ -3,18 +3,17 @@ from __future__ import annotations import uuid from datetime import datetime, timezone from decimal import Decimal -from sqlalchemy import String, Integer, Numeric, DateTime, Boolean, Text, ForeignKey, Index +from sqlalchemy import String, Integer, Numeric, DateTime, Boolean, Text, ForeignKey, Index, Uuid from sqlalchemy.orm import Mapped, mapped_column -from sqlalchemy.dialects.postgresql import UUID -from app.database import Base +from diligence.database import Base class NutritionGoal(Base): """Per-user macro + eating-window targets. One active per user.""" __tablename__ = "nutrition_goals" - id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - user_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False, unique=True) + id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, default=uuid.uuid4) + user_id: Mapped[uuid.UUID] = mapped_column(Uuid, ForeignKey("users.id"), nullable=False, unique=True) # Macro targets (grams / kcal) calorie_target: Mapped[int] = mapped_column(Integer, default=2400) @@ -44,8 +43,8 @@ class Fast(Base): """A single fasting bout — start/end + type + compliance flag.""" __tablename__ = "fasts" - id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - user_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False) + id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, default=uuid.uuid4) + user_id: Mapped[uuid.UUID] = mapped_column(Uuid, ForeignKey("users.id"), nullable=False) started_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False) ended_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) @@ -60,7 +59,7 @@ class Fast(Base): # Points granted (for idempotency — don't double-credit) points_awarded: Mapped[int] = mapped_column(Integer, default=0) - activity_log_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("activity_log.id")) + activity_log_id: Mapped[uuid.UUID | None] = mapped_column(Uuid, ForeignKey("activity_log.id")) notes: Mapped[str | None] = mapped_column(Text) @@ -75,8 +74,8 @@ class ElectrolyteLog(Base): """Daily electrolyte dosing checklist — lightweight tracker.""" __tablename__ = "electrolyte_log" - id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - user_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False) + id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, default=uuid.uuid4) + user_id: Mapped[uuid.UUID] = mapped_column(Uuid, ForeignKey("users.id"), nullable=False) log_date: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False) sodium_mg: Mapped[int] = mapped_column(Integer, default=0) diff --git a/backend/app/models/oauth.py b/diligence/models/oauth.py similarity index 77% rename from backend/app/models/oauth.py rename to diligence/models/oauth.py index 59d5450..9cac541 100644 --- a/backend/app/models/oauth.py +++ b/diligence/models/oauth.py @@ -2,17 +2,16 @@ from __future__ import annotations import uuid from datetime import datetime, timezone -from sqlalchemy import String, Text, DateTime, ForeignKey, UniqueConstraint +from sqlalchemy import String, Text, DateTime, ForeignKey, UniqueConstraint, Uuid from sqlalchemy.orm import Mapped, mapped_column -from sqlalchemy.dialects.postgresql import UUID -from app.database import Base +from diligence.database import Base class OAuthToken(Base): __tablename__ = "oauth_tokens" - id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - user_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False) + id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, default=uuid.uuid4) + user_id: Mapped[uuid.UUID] = mapped_column(Uuid, ForeignKey("users.id"), nullable=False) provider: Mapped[str] = mapped_column(String(20), nullable=False) access_token: Mapped[str] = mapped_column(Text, nullable=False) refresh_token: Mapped[str | None] = mapped_column(Text) diff --git a/backend/app/models/points.py b/diligence/models/points.py similarity index 81% rename from backend/app/models/points.py rename to diligence/models/points.py index 1c3bc56..cd6eddc 100644 --- a/backend/app/models/points.py +++ b/diligence/models/points.py @@ -2,17 +2,16 @@ from __future__ import annotations import uuid from datetime import datetime, timezone -from sqlalchemy import String, Integer, Boolean, DateTime, ForeignKey +from sqlalchemy import String, Integer, Boolean, DateTime, ForeignKey, Uuid from sqlalchemy.orm import Mapped, mapped_column, relationship -from sqlalchemy.dialects.postgresql import UUID -from app.database import Base +from diligence.database import Base class PointRule(Base): __tablename__ = "point_rules" - id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - user_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False) + id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, default=uuid.uuid4) + user_id: Mapped[uuid.UUID] = mapped_column(Uuid, ForeignKey("users.id"), nullable=False) category: Mapped[str] = mapped_column(String(50), nullable=False) description: Mapped[str] = mapped_column(String(200), nullable=False) points: Mapped[int] = mapped_column(Integer, nullable=False) @@ -26,8 +25,8 @@ class PointRule(Base): class DailyTarget(Base): __tablename__ = "daily_targets" - id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - user_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False, unique=True) + id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, default=uuid.uuid4) + user_id: Mapped[uuid.UUID] = mapped_column(Uuid, ForeignKey("users.id"), nullable=False, unique=True) daily_minimum_pts: Mapped[int] = mapped_column(Integer, default=80) weekly_target_pts: Mapped[int] = mapped_column(Integer, default=500) weekly_bonus_pts: Mapped[int] = mapped_column(Integer, default=50) diff --git a/backend/app/models/profile.py b/diligence/models/profile.py similarity index 81% rename from backend/app/models/profile.py rename to diligence/models/profile.py index fd8ef5a..07bac20 100644 --- a/backend/app/models/profile.py +++ b/diligence/models/profile.py @@ -3,17 +3,16 @@ from __future__ import annotations import uuid from datetime import datetime, timezone from decimal import Decimal -from sqlalchemy import String, Boolean, Integer, Numeric, DateTime, Text, ForeignKey +from sqlalchemy import String, Boolean, Integer, Numeric, DateTime, Text, ForeignKey, Uuid, JSON from sqlalchemy.orm import Mapped, mapped_column, relationship -from sqlalchemy.dialects.postgresql import UUID, JSONB -from app.database import Base +from diligence.database import Base class UserProfile(Base): __tablename__ = "user_profiles" - id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - user_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False, unique=True) + id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, default=uuid.uuid4) + user_id: Mapped[uuid.UUID] = mapped_column(Uuid, ForeignKey("users.id"), nullable=False, unique=True) # Phase 1 primary_goal: Mapped[str | None] = mapped_column(String(50)) @@ -41,9 +40,9 @@ class UserProfile(Base): motivation_rai: Mapped[Decimal | None] = mapped_column(Numeric(5, 1)) # Preferences - activity_preferences: Mapped[dict] = mapped_column(JSONB, default=list) + activity_preferences: Mapped[dict] = mapped_column(JSON, default=list) equipment_access: Mapped[str | None] = mapped_column(String(50)) # legacy — kept for compat - equipment_list: Mapped[dict] = mapped_column(JSONB, default=list) # new: list of equipment strings + equipment_list: Mapped[dict] = mapped_column(JSON, default=list) # new: list of equipment strings days_per_week: Mapped[int] = mapped_column(Integer, default=3) minutes_per_session: Mapped[int] = mapped_column(Integer, default=30) diff --git a/backend/app/models/program.py b/diligence/models/program.py similarity index 75% rename from backend/app/models/program.py rename to diligence/models/program.py index d343faf..174398c 100644 --- a/backend/app/models/program.py +++ b/diligence/models/program.py @@ -2,17 +2,16 @@ from __future__ import annotations import uuid from datetime import datetime, date, timezone -from sqlalchemy import String, Integer, Text, Date, DateTime, ForeignKey +from sqlalchemy import String, Integer, Text, Date, DateTime, ForeignKey, Uuid from sqlalchemy.orm import Mapped, mapped_column, relationship -from sqlalchemy.dialects.postgresql import UUID -from app.database import Base +from diligence.database import Base class Program(Base): __tablename__ = "programs" - id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - user_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False) + id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, default=uuid.uuid4) + user_id: Mapped[uuid.UUID] = mapped_column(Uuid, ForeignKey("users.id"), nullable=False) name: Mapped[str] = mapped_column(String(200), nullable=False) source: Mapped[str | None] = mapped_column(String(50)) source_url: Mapped[str | None] = mapped_column(Text) @@ -24,7 +23,7 @@ class Program(Base): # v2: Link to catalog program catalog_id: Mapped[uuid.UUID | None] = mapped_column( - UUID(as_uuid=True), ForeignKey("program_catalog.id"), nullable=True + Uuid, ForeignKey("program_catalog.id"), nullable=True ) current_week: Mapped[int] = mapped_column(Integer, default=1) current_day: Mapped[int] = mapped_column(Integer, default=1) diff --git a/backend/app/models/resource.py b/diligence/models/resource.py similarity index 66% rename from backend/app/models/resource.py rename to diligence/models/resource.py index 7a52147..398e3d9 100644 --- a/backend/app/models/resource.py +++ b/diligence/models/resource.py @@ -2,25 +2,24 @@ from __future__ import annotations import uuid from datetime import datetime, timezone -from sqlalchemy import String, Integer, Boolean, Text, DateTime +from sqlalchemy import String, Integer, Boolean, Text, DateTime, Uuid, JSON from sqlalchemy.orm import Mapped, mapped_column -from sqlalchemy.dialects.postgresql import UUID, JSONB -from app.database import Base +from diligence.database import Base class Resource(Base): __tablename__ = "resource_library" - id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, default=uuid.uuid4) name: Mapped[str] = mapped_column(String(200), nullable=False) source: Mapped[str] = mapped_column(String(50), nullable=False) url: Mapped[str] = mapped_column(Text, nullable=False) description: Mapped[str | None] = mapped_column(Text) thumbnail_url: Mapped[str | None] = mapped_column(Text) - goal_tags: Mapped[dict] = mapped_column(JSONB, default=list) - activity_tags: Mapped[dict] = mapped_column(JSONB, default=list) + goal_tags: Mapped[dict] = mapped_column(JSON, default=list) + activity_tags: Mapped[dict] = mapped_column(JSON, default=list) equipment_needed: Mapped[str | None] = mapped_column(String(50)) - ttm_stages: Mapped[dict] = mapped_column(JSONB, default=list) + ttm_stages: Mapped[dict] = mapped_column(JSON, default=list) difficulty: Mapped[str | None] = mapped_column(String(20)) duration_days: Mapped[int | None] = mapped_column(Integer) is_active: Mapped[bool] = mapped_column(Boolean, default=True) diff --git a/backend/app/models/reward.py b/diligence/models/reward.py similarity index 65% rename from backend/app/models/reward.py rename to diligence/models/reward.py index bfd2ea6..0ee91b8 100644 --- a/backend/app/models/reward.py +++ b/diligence/models/reward.py @@ -2,17 +2,16 @@ from __future__ import annotations import uuid from datetime import datetime, date, timezone -from sqlalchemy import String, Integer, Boolean, Text, Date, DateTime, ForeignKey, Index +from sqlalchemy import String, Integer, Boolean, Text, Date, DateTime, ForeignKey, Index, Uuid from sqlalchemy.orm import Mapped, mapped_column, relationship -from sqlalchemy.dialects.postgresql import UUID -from app.database import Base +from diligence.database import Base class Reward(Base): __tablename__ = "rewards" - id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - user_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False) + id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, default=uuid.uuid4) + user_id: Mapped[uuid.UUID] = mapped_column(Uuid, ForeignKey("users.id"), nullable=False) name: Mapped[str] = mapped_column(String(200), nullable=False) description: Mapped[str | None] = mapped_column(Text) point_cost: Mapped[int] = mapped_column(Integer, nullable=False) @@ -25,9 +24,9 @@ class Reward(Base): class RewardRedemption(Base): __tablename__ = "reward_redemptions" - id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - user_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False) - reward_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("rewards.id"), nullable=False) + id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, default=uuid.uuid4) + user_id: Mapped[uuid.UUID] = mapped_column(Uuid, ForeignKey("users.id"), nullable=False) + reward_id: Mapped[uuid.UUID] = mapped_column(Uuid, ForeignKey("rewards.id"), nullable=False) points_spent: Mapped[int] = mapped_column(Integer, nullable=False) redeemed_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)) redemption_date: Mapped[date] = mapped_column(Date, nullable=False) diff --git a/backend/app/models/support.py b/diligence/models/support.py similarity index 75% rename from backend/app/models/support.py rename to diligence/models/support.py index 9ee52ab..ef61637 100644 --- a/backend/app/models/support.py +++ b/diligence/models/support.py @@ -2,19 +2,18 @@ from __future__ import annotations import uuid from datetime import datetime, timezone -from sqlalchemy import String, Integer, Text, DateTime, ForeignKey, Index, UniqueConstraint +from sqlalchemy import String, Integer, Text, DateTime, ForeignKey, Index, UniqueConstraint, Uuid, JSON from sqlalchemy.orm import Mapped, mapped_column, relationship -from sqlalchemy.dialects.postgresql import UUID, JSONB -from app.database import Base +from diligence.database import Base class SupportThread(Base): """One thread per user — private conversation with admin.""" __tablename__ = "support_threads" - id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, default=uuid.uuid4) user_id: Mapped[uuid.UUID] = mapped_column( - UUID(as_uuid=True), ForeignKey("users.id"), unique=True, nullable=False + Uuid, ForeignKey("users.id"), unique=True, nullable=False ) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=lambda: datetime.now(timezone.utc) @@ -35,13 +34,13 @@ class SupportMessage(Base): """Individual message within a support thread.""" __tablename__ = "support_messages" - id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, default=uuid.uuid4) thread_id: Mapped[uuid.UUID] = mapped_column( - UUID(as_uuid=True), ForeignKey("support_threads.id", ondelete="CASCADE"), nullable=False + Uuid, ForeignKey("support_threads.id", ondelete="CASCADE"), nullable=False ) sender: Mapped[str] = mapped_column(String(10), nullable=False) # 'user' or 'admin' body: Mapped[str] = mapped_column(Text, nullable=False) - context: Mapped[dict | None] = mapped_column(JSONB) + context: Mapped[dict | None] = mapped_column(JSON) read_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=lambda: datetime.now(timezone.utc) diff --git a/backend/app/models/user.py b/diligence/models/user.py similarity index 84% rename from backend/app/models/user.py rename to diligence/models/user.py index 104eebb..1f7601f 100644 --- a/backend/app/models/user.py +++ b/diligence/models/user.py @@ -2,16 +2,15 @@ from __future__ import annotations import uuid from datetime import datetime, timezone -from sqlalchemy import String, Boolean, DateTime +from sqlalchemy import String, Boolean, DateTime, Uuid from sqlalchemy.orm import Mapped, mapped_column, relationship -from sqlalchemy.dialects.postgresql import UUID -from app.database import Base +from diligence.database import Base class User(Base): __tablename__ = "users" - id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, default=uuid.uuid4) username: Mapped[str] = mapped_column(String(50), unique=True, nullable=False) display_name: Mapped[str] = mapped_column(String(100), nullable=False) password_hash: Mapped[str] = mapped_column(String(255), nullable=False) diff --git a/backend/app/__init__.py b/diligence/routers/__init__.py similarity index 100% rename from backend/app/__init__.py rename to diligence/routers/__init__.py diff --git a/backend/app/routers/activities.py b/diligence/routers/activities.py similarity index 89% rename from backend/app/routers/activities.py rename to diligence/routers/activities.py index fde5c5a..aec9942 100644 --- a/backend/app/routers/activities.py +++ b/diligence/routers/activities.py @@ -6,12 +6,12 @@ from fastapi import APIRouter, Depends, Query from sqlalchemy import select, delete from sqlalchemy.ext.asyncio import AsyncSession -from app.database import get_db -from app.models.user import User -from app.models.activity import ActivityLog -from app.schemas.activity import ActivityCreate, ActivityResponse -from app.utils.auth import get_current_user -from app.services.points_engine import log_activity_with_points +from diligence.database import get_db +from diligence.models.user import User +from diligence.models.activity import ActivityLog +from diligence.schemas.activity import ActivityCreate, ActivityResponse +from diligence.utils.auth import get_current_user +from diligence.services.points_engine import log_activity_with_points router = APIRouter(prefix="/api/activities", tags=["activities"]) diff --git a/backend/app/routers/ai_chat.py b/diligence/routers/ai_chat.py similarity index 92% rename from backend/app/routers/ai_chat.py rename to diligence/routers/ai_chat.py index 1e2bcd8..28a737d 100644 --- a/backend/app/routers/ai_chat.py +++ b/diligence/routers/ai_chat.py @@ -7,10 +7,10 @@ from fastapi import APIRouter, Depends, Request from fastapi.responses import StreamingResponse from sqlalchemy.ext.asyncio import AsyncSession -from app.database import get_db -from app.utils.auth import get_current_user -from app.models.user import User -from app.services import ai_provider +from diligence.database import get_db +from diligence.utils.auth import get_current_user +from diligence.models.user import User +from diligence.services import ai_provider logger = logging.getLogger(__name__) router = APIRouter(prefix="/api/ai", tags=["AI Coaching"]) diff --git a/backend/app/routers/auth.py b/diligence/routers/auth.py similarity index 87% rename from backend/app/routers/auth.py rename to diligence/routers/auth.py index 517998d..d283ab1 100644 --- a/backend/app/routers/auth.py +++ b/diligence/routers/auth.py @@ -7,12 +7,12 @@ from fastapi.responses import JSONResponse from sqlalchemy import select, func from sqlalchemy.ext.asyncio import AsyncSession -from app.database import get_db -from app.models.user import User -from app.models.profile import UserProfile -from app.models.points import PointRule, DailyTarget, DEFAULT_POINT_RULES, TTM_DAILY_TARGETS -from app.schemas.auth import LoginRequest, RegisterRequest, TokenResponse, UserResponse -from app.utils.auth import hash_password, verify_password, create_access_token, get_current_user +from diligence.database import get_db +from diligence.models.user import User +from diligence.models.profile import UserProfile +from diligence.models.points import PointRule, DailyTarget, DEFAULT_POINT_RULES, TTM_DAILY_TARGETS +from diligence.schemas.auth import LoginRequest, RegisterRequest, TokenResponse, UserResponse +from diligence.utils.auth import hash_password, verify_password, create_access_token, get_current_user logger = logging.getLogger("fitness-rewards") router = APIRouter(prefix="/api/auth", tags=["auth"]) diff --git a/backend/app/routers/catalog.py b/diligence/routers/catalog.py similarity index 98% rename from backend/app/routers/catalog.py rename to diligence/routers/catalog.py index a1b5fe1..fe555c5 100644 --- a/backend/app/routers/catalog.py +++ b/diligence/routers/catalog.py @@ -11,13 +11,13 @@ from sqlalchemy import select, func from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload -from app.database import get_db -from app.models.user import User -from app.models.program import Program -from app.models.catalog import ProgramCatalog, CatalogWorkout, CrawlQueue, WorkoutLog -from app.services.program_research import slugify, find_urls_for_program -from app.services.points_engine import log_activity_with_points -from app.utils.auth import get_current_user +from diligence.database import get_db +from diligence.models.user import User +from diligence.models.program import Program +from diligence.models.catalog import ProgramCatalog, CatalogWorkout, CrawlQueue, WorkoutLog +from diligence.services.program_research import slugify, find_urls_for_program +from diligence.services.points_engine import log_activity_with_points +from diligence.utils.auth import get_current_user router = APIRouter(prefix="/api/programs", tags=["programs-v2"]) diff --git a/backend/app/routers/food.py b/diligence/routers/food.py similarity index 92% rename from backend/app/routers/food.py rename to diligence/routers/food.py index 3ec4fda..d0724ba 100644 --- a/backend/app/routers/food.py +++ b/diligence/routers/food.py @@ -7,12 +7,12 @@ from fastapi import APIRouter, Depends, Query, HTTPException from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession -from app.database import get_db -from app.models.user import User -from app.models.food import FoodLog -from app.schemas.food import FoodCreate, FoodSearchResult -from app.utils.auth import get_current_user -from app.services.food_lookup import lookup_barcode, search_food +from diligence.database import get_db +from diligence.models.user import User +from diligence.models.food import FoodLog +from diligence.schemas.food import FoodCreate, FoodSearchResult +from diligence.utils.auth import get_current_user +from diligence.services.food_lookup import lookup_barcode, search_food router = APIRouter(prefix="/api/food", tags=["food"]) diff --git a/backend/app/routers/integrations.py b/diligence/routers/integrations.py similarity index 92% rename from backend/app/routers/integrations.py rename to diligence/routers/integrations.py index 0aa52a5..5b705a2 100644 --- a/backend/app/routers/integrations.py +++ b/diligence/routers/integrations.py @@ -8,14 +8,14 @@ from fastapi.responses import RedirectResponse from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession -from app.database import get_db -from app.models.user import User -from app.models.oauth import OAuthToken -from app.utils.auth import get_current_user -from app.services.strava_sync import ( +from diligence.database import get_db +from diligence.models.user import User +from diligence.models.oauth import OAuthToken +from diligence.utils.auth import get_current_user +from diligence.services.strava_sync import ( get_strava_auth_url, exchange_strava_code, sync_strava_activities, ) -from app.services.polar_sync import ( +from diligence.services.polar_sync import ( get_polar_auth_url, exchange_polar_code, register_polar_user, sync_polar_activities, ) @@ -39,7 +39,7 @@ async def integration_status( # --- Strava --- @router.get("/strava/auth") async def strava_auth(user: Annotated[User, Depends(get_current_user)]): - from app.utils.auth import create_access_token + from diligence.utils.auth import create_access_token from datetime import timedelta state_token = create_access_token(str(user.id), expires_delta=timedelta(minutes=10)) return {"auth_url": get_strava_auth_url(state_token)} @@ -52,7 +52,7 @@ async def strava_callback( db: AsyncSession = Depends(get_db), ): from jose import JWTError, jwt as jose_jwt - from app.config import get_settings + from diligence.config import get_settings _settings = get_settings() try: payload = jose_jwt.decode(state, _settings.secret_key, algorithms=["HS256"]) @@ -85,7 +85,7 @@ async def strava_callback( db.add(token) await db.flush() - from app.config import get_settings + from diligence.config import get_settings return RedirectResponse(url=f"{get_settings().base_url}/settings/integrations?strava=connected") @@ -101,7 +101,7 @@ async def strava_sync( # --- Polar --- @router.get("/polar/auth") async def polar_auth(user: Annotated[User, Depends(get_current_user)]): - from app.utils.auth import create_access_token + from diligence.utils.auth import create_access_token from datetime import timedelta state_token = create_access_token(str(user.id), expires_delta=timedelta(minutes=10)) return {"auth_url": get_polar_auth_url(state_token)} @@ -114,7 +114,7 @@ async def polar_callback( db: AsyncSession = Depends(get_db), ): from jose import JWTError, jwt as jose_jwt - from app.config import get_settings + from diligence.config import get_settings _settings = get_settings() try: payload = jose_jwt.decode(state, _settings.secret_key, algorithms=["HS256"]) @@ -144,7 +144,7 @@ async def polar_callback( db.add(token) await db.flush() - from app.config import get_settings + from diligence.config import get_settings return RedirectResponse(url=f"{get_settings().base_url}/settings/integrations?polar=connected") @@ -178,10 +178,10 @@ async def disconnect( # --- Dynamic Integration Config (v3) --- from pydantic import BaseModel -from app.models.integration_config import IntegrationConfig -from app.services.provider_registry import PROVIDER_REGISTRY -from app.services.crypto import encrypt_value, decrypt_value -from app.config import settings +from diligence.models.integration_config import IntegrationConfig +from diligence.services.provider_registry import PROVIDER_REGISTRY +from diligence.services.crypto import encrypt_value, decrypt_value +from diligence.config import settings class ConfigureRequest(BaseModel): diff --git a/backend/app/routers/meal_plans.py b/diligence/routers/meal_plans.py similarity index 97% rename from backend/app/routers/meal_plans.py rename to diligence/routers/meal_plans.py index c89fe65..12d02e3 100644 --- a/backend/app/routers/meal_plans.py +++ b/diligence/routers/meal_plans.py @@ -10,10 +10,10 @@ from pydantic import BaseModel from sqlalchemy import select, func from sqlalchemy.ext.asyncio import AsyncSession -from app.database import get_db -from app.models.user import User -from app.models.meal_plan import MealPlan, MealPlanItem, MealCompliance -from app.utils.auth import get_current_user +from diligence.database import get_db +from diligence.models.user import User +from diligence.models.meal_plan import MealPlan, MealPlanItem, MealCompliance +from diligence.utils.auth import get_current_user router = APIRouter(prefix="/api/meal-plans", tags=["meal-plans"]) diff --git a/backend/app/routers/nutrition.py b/diligence/routers/nutrition.py similarity index 97% rename from backend/app/routers/nutrition.py rename to diligence/routers/nutrition.py index 0694741..636db03 100644 --- a/backend/app/routers/nutrition.py +++ b/diligence/routers/nutrition.py @@ -10,15 +10,15 @@ from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy import select, func, and_ from sqlalchemy.ext.asyncio import AsyncSession -from app.database import get_db -from app.models.user import User -from app.models.nutrition import NutritionGoal, Fast, ElectrolyteLog -from app.models.food import FoodLog -from app.schemas.nutrition import ( +from diligence.database import get_db +from diligence.models.user import User +from diligence.models.nutrition import NutritionGoal, Fast, ElectrolyteLog +from diligence.models.food import FoodLog +from diligence.schemas.nutrition import ( NutritionGoalIn, NutritionGoalOut, FastStart, FastUpdate, FastOut, ElectrolyteIn, ) -from app.utils.auth import get_current_user -from app.services.points_engine import log_activity_with_points +from diligence.utils.auth import get_current_user +from diligence.services.points_engine import log_activity_with_points router = APIRouter(prefix="/api/nutrition", tags=["nutrition"]) diff --git a/backend/app/routers/onboarding.py b/diligence/routers/onboarding.py similarity index 92% rename from backend/app/routers/onboarding.py rename to diligence/routers/onboarding.py index 79abeb6..487a445 100644 --- a/backend/app/routers/onboarding.py +++ b/diligence/routers/onboarding.py @@ -6,13 +6,13 @@ from fastapi import APIRouter, Depends from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession -from app.database import get_db -from app.models.user import User -from app.models.profile import UserProfile -from app.models.points import DailyTarget, TTM_DAILY_TARGETS -from app.schemas.onboarding import Phase1Request, Phase2Request, OnboardingStatusResponse -from app.utils.auth import get_current_user -from app.services.resource_matcher import get_recommendations +from diligence.database import get_db +from diligence.models.user import User +from diligence.models.profile import UserProfile +from diligence.models.points import DailyTarget, TTM_DAILY_TARGETS +from diligence.schemas.onboarding import Phase1Request, Phase2Request, OnboardingStatusResponse +from diligence.utils.auth import get_current_user +from diligence.services.resource_matcher import get_recommendations router = APIRouter(prefix="/api/onboarding", tags=["onboarding"]) diff --git a/backend/app/routers/points.py b/diligence/routers/points.py similarity index 90% rename from backend/app/routers/points.py rename to diligence/routers/points.py index 76c6b32..6bbc34e 100644 --- a/backend/app/routers/points.py +++ b/diligence/routers/points.py @@ -7,14 +7,14 @@ from fastapi import APIRouter, Depends, Query, HTTPException from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession -from app.database import get_db -from app.models.user import User -from app.models.points import PointRule, DailyTarget -from app.models.reward import Reward -from app.schemas.points import PointRuleUpdate, DailyTargetUpdate -from app.schemas.reward import RewardCreate, RewardRedeemRequest -from app.utils.auth import get_current_user -from app.services.points_engine import get_today_status, get_weekly_summary, redeem_reward +from diligence.database import get_db +from diligence.models.user import User +from diligence.models.points import PointRule, DailyTarget +from diligence.models.reward import Reward +from diligence.schemas.points import PointRuleUpdate, DailyTargetUpdate +from diligence.schemas.reward import RewardCreate, RewardRedeemRequest +from diligence.utils.auth import get_current_user +from diligence.services.points_engine import get_today_status, get_weekly_summary, redeem_reward router = APIRouter(prefix="/api/points", tags=["points"]) diff --git a/backend/app/routers/programs.py b/diligence/routers/programs.py similarity index 94% rename from backend/app/routers/programs.py rename to diligence/routers/programs.py index 2f42359..fc7d1c2 100644 --- a/backend/app/routers/programs.py +++ b/diligence/routers/programs.py @@ -7,11 +7,11 @@ from fastapi import APIRouter, Depends, HTTPException from sqlalchemy import select, func from sqlalchemy.ext.asyncio import AsyncSession -from app.database import get_db -from app.models.user import User -from app.models.program import Program -from app.models.activity import ActivityLog -from app.utils.auth import get_current_user +from diligence.database import get_db +from diligence.models.user import User +from diligence.models.program import Program +from diligence.models.activity import ActivityLog +from diligence.utils.auth import get_current_user from pydantic import BaseModel router = APIRouter(prefix="/api/programs", tags=["programs"]) diff --git a/backend/app/routers/support.py b/diligence/routers/support.py similarity index 96% rename from backend/app/routers/support.py rename to diligence/routers/support.py index c8cb767..4a35ffa 100644 --- a/backend/app/routers/support.py +++ b/diligence/routers/support.py @@ -13,13 +13,13 @@ from sqlalchemy import select, func, update from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload -from app.database import get_db -from app.models.user import User -from app.models.support import SupportThread, SupportMessage -from app.models.activity import ActivityLog -from app.services.points_engine import get_active_program, get_today_status -from app.utils.auth import get_current_user -from app.config import settings +from diligence.database import get_db +from diligence.models.user import User +from diligence.models.support import SupportThread, SupportMessage +from diligence.models.activity import ActivityLog +from diligence.services.points_engine import get_active_program, get_today_status +from diligence.utils.auth import get_current_user +from diligence.config import settings logger = logging.getLogger(__name__) router = APIRouter(prefix="/api/support", tags=["support"]) @@ -301,7 +301,7 @@ async def list_threads( out = [] for t in threads: # Get the user's display name - from app.models.user import User as UserModel + from diligence.models.user import User as UserModel user_result = await db.execute( select(UserModel).where(UserModel.id == t.user_id) ) @@ -360,7 +360,7 @@ async def get_admin_thread( await db.flush() # Get user info - from app.models.user import User as UserModel + from diligence.models.user import User as UserModel user_result = await db.execute( select(UserModel).where(UserModel.id == thread.user_id) ) diff --git a/backend/app/routers/__init__.py b/diligence/schemas/__init__.py similarity index 100% rename from backend/app/routers/__init__.py rename to diligence/schemas/__init__.py diff --git a/backend/app/schemas/activity.py b/diligence/schemas/activity.py similarity index 100% rename from backend/app/schemas/activity.py rename to diligence/schemas/activity.py diff --git a/backend/app/schemas/auth.py b/diligence/schemas/auth.py similarity index 100% rename from backend/app/schemas/auth.py rename to diligence/schemas/auth.py diff --git a/backend/app/schemas/food.py b/diligence/schemas/food.py similarity index 100% rename from backend/app/schemas/food.py rename to diligence/schemas/food.py diff --git a/backend/app/schemas/nutrition.py b/diligence/schemas/nutrition.py similarity index 100% rename from backend/app/schemas/nutrition.py rename to diligence/schemas/nutrition.py diff --git a/backend/app/schemas/onboarding.py b/diligence/schemas/onboarding.py similarity index 100% rename from backend/app/schemas/onboarding.py rename to diligence/schemas/onboarding.py diff --git a/backend/app/schemas/points.py b/diligence/schemas/points.py similarity index 100% rename from backend/app/schemas/points.py rename to diligence/schemas/points.py diff --git a/backend/app/schemas/reward.py b/diligence/schemas/reward.py similarity index 100% rename from backend/app/schemas/reward.py rename to diligence/schemas/reward.py diff --git a/backend/app/schemas/__init__.py b/diligence/services/__init__.py similarity index 100% rename from backend/app/schemas/__init__.py rename to diligence/services/__init__.py diff --git a/backend/app/services/ai_provider.py b/diligence/services/ai_provider.py similarity index 97% rename from backend/app/services/ai_provider.py rename to diligence/services/ai_provider.py index ba6d160..ad096f1 100644 --- a/backend/app/services/ai_provider.py +++ b/diligence/services/ai_provider.py @@ -18,9 +18,9 @@ import httpx from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession -from app.config import get_settings -from app.services.crypto import decrypt_value -from app.models.integration_config import IntegrationConfig +from diligence.config import get_settings +from diligence.services.crypto import decrypt_value +from diligence.models.integration_config import IntegrationConfig logger = logging.getLogger(__name__) settings = get_settings() @@ -101,8 +101,8 @@ async def get_active_ai_provider(db: AsyncSession, user_id=None) -> dict | None: async def build_context(db: AsyncSession, user_id) -> str: """Build live context string from user's current state.""" - from app.models.user import User - from app.models.profile import UserProfile + from diligence.models.user import User + from diligence.models.profile import UserProfile context_parts = [] @@ -122,7 +122,7 @@ async def build_context(db: AsyncSession, user_id) -> str: # Today's points (best effort) try: - from app.services.points_engine import get_daily_summary + from diligence.services.points_engine import get_daily_summary summary = await get_daily_summary(db, user_id) context_parts.append(f"Today's points: {summary.get('earned', 0)}/{summary.get('target', 100)}") context_parts.append(f"Daily gate: {'PASSED' if summary.get('gate_passed') else 'NOT YET'}") diff --git a/backend/app/services/crawl_scheduler.py b/diligence/services/crawl_scheduler.py similarity index 94% rename from backend/app/services/crawl_scheduler.py rename to diligence/services/crawl_scheduler.py index a2a56ed..27c721a 100644 --- a/backend/app/services/crawl_scheduler.py +++ b/diligence/services/crawl_scheduler.py @@ -8,9 +8,9 @@ from datetime import datetime, timezone from sqlalchemy import select, func from sqlalchemy.ext.asyncio import AsyncSession -from app.database import async_session -from app.models.catalog import CrawlQueue -from app.services.program_research import process_crawl_job +from diligence.database import async_session +from diligence.models.catalog import CrawlQueue +from diligence.services.program_research import process_crawl_job logger = logging.getLogger(__name__) diff --git a/backend/app/services/crypto.py b/diligence/services/crypto.py similarity index 100% rename from backend/app/services/crypto.py rename to diligence/services/crypto.py diff --git a/backend/app/services/device_sync_base.py b/diligence/services/device_sync_base.py similarity index 94% rename from backend/app/services/device_sync_base.py rename to diligence/services/device_sync_base.py index b88bd25..7692ab9 100644 --- a/backend/app/services/device_sync_base.py +++ b/diligence/services/device_sync_base.py @@ -17,11 +17,11 @@ import httpx from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession -from app.config import get_settings -from app.models.oauth import OAuthToken -from app.models.integration_config import IntegrationConfig -from app.services.crypto import decrypt_value -from app.services.points_engine import log_activity_with_points +from diligence.config import get_settings +from diligence.models.oauth import OAuthToken +from diligence.models.integration_config import IntegrationConfig +from diligence.services.crypto import decrypt_value +from diligence.services.points_engine import log_activity_with_points logger = logging.getLogger(__name__) settings = get_settings() @@ -146,7 +146,7 @@ class DeviceSyncBase(abc.ABC): Deduplicates by (user_id, provider, external_id). """ - from app.models.activity import ActivityLog + from diligence.models.activity import ActivityLog if external_id: existing = await db.execute( diff --git a/backend/app/services/food_lookup.py b/diligence/services/food_lookup.py similarity index 98% rename from backend/app/services/food_lookup.py rename to diligence/services/food_lookup.py index e499945..9d3221a 100644 --- a/backend/app/services/food_lookup.py +++ b/diligence/services/food_lookup.py @@ -2,7 +2,7 @@ from __future__ import annotations """Open Food Facts API client for barcode scanning and food search.""" import httpx -from app.schemas.food import FoodSearchResult +from diligence.schemas.food import FoodSearchResult OFF_BASE = "https://world.openfoodfacts.org" USER_AGENT = "Diligence/1.0" diff --git a/backend/app/services/points_engine.py b/diligence/services/points_engine.py similarity index 97% rename from backend/app/services/points_engine.py rename to diligence/services/points_engine.py index 07acacc..eb996db 100644 --- a/backend/app/services/points_engine.py +++ b/diligence/services/points_engine.py @@ -6,11 +6,11 @@ from datetime import date, timedelta from sqlalchemy import select, func from sqlalchemy.ext.asyncio import AsyncSession -from app.models.activity import ActivityLog -from app.models.points import PointRule, DailyTarget -from app.models.reward import Reward, RewardRedemption -from app.models.program import Program -from app.utils.dates import get_week_boundaries +from diligence.models.activity import ActivityLog +from diligence.models.points import PointRule, DailyTarget +from diligence.models.reward import Reward, RewardRedemption +from diligence.models.program import Program +from diligence.utils.dates import get_week_boundaries async def get_daily_points_earned(db: AsyncSession, user_id: uuid.UUID, d: date) -> int: diff --git a/backend/app/services/polar_sync.py b/diligence/services/polar_sync.py similarity index 96% rename from backend/app/services/polar_sync.py rename to diligence/services/polar_sync.py index a63c856..a840c73 100644 --- a/backend/app/services/polar_sync.py +++ b/diligence/services/polar_sync.py @@ -7,10 +7,10 @@ import httpx from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession -from app.config import get_settings -from app.models.oauth import OAuthToken -from app.models.activity import ActivityLog -from app.services.points_engine import log_activity_with_points +from diligence.config import get_settings +from diligence.models.oauth import OAuthToken +from diligence.models.activity import ActivityLog +from diligence.services.points_engine import log_activity_with_points settings = get_settings() diff --git a/backend/app/services/program_research.py b/diligence/services/program_research.py similarity index 98% rename from backend/app/services/program_research.py rename to diligence/services/program_research.py index d24bcd0..e1ab220 100644 --- a/backend/app/services/program_research.py +++ b/diligence/services/program_research.py @@ -12,8 +12,8 @@ import httpx from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession -from app.models.catalog import ProgramCatalog, CatalogWorkout, CrawlQueue -from app.config import settings +from diligence.models.catalog import ProgramCatalog, CatalogWorkout, CrawlQueue +from diligence.config import settings # ── Known program sources (skip search for these) ────────────────────────── diff --git a/backend/app/services/provider_registry.py b/diligence/services/provider_registry.py similarity index 100% rename from backend/app/services/provider_registry.py rename to diligence/services/provider_registry.py diff --git a/backend/app/services/resource_matcher.py b/diligence/services/resource_matcher.py similarity index 96% rename from backend/app/services/resource_matcher.py rename to diligence/services/resource_matcher.py index b55d1b0..221eec7 100644 --- a/backend/app/services/resource_matcher.py +++ b/diligence/services/resource_matcher.py @@ -4,8 +4,8 @@ from __future__ import annotations import uuid from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession -from app.models.resource import Resource -from app.models.profile import UserProfile +from diligence.models.resource import Resource +from diligence.models.profile import UserProfile # Equipment categories that satisfy each resource requirement diff --git a/backend/app/services/strava_sync.py b/diligence/services/strava_sync.py similarity index 96% rename from backend/app/services/strava_sync.py rename to diligence/services/strava_sync.py index 2f509ce..a4937bb 100644 --- a/backend/app/services/strava_sync.py +++ b/diligence/services/strava_sync.py @@ -7,10 +7,10 @@ import httpx from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession -from app.config import get_settings -from app.models.oauth import OAuthToken -from app.models.activity import ActivityLog -from app.services.points_engine import log_activity_with_points +from diligence.config import get_settings +from diligence.models.oauth import OAuthToken +from diligence.models.activity import ActivityLog +from diligence.services.points_engine import log_activity_with_points settings = get_settings() diff --git a/backend/app/services/usda_lookup.py b/diligence/services/usda_lookup.py similarity index 100% rename from backend/app/services/usda_lookup.py rename to diligence/services/usda_lookup.py diff --git a/backend/app/services/__init__.py b/diligence/utils/__init__.py similarity index 100% rename from backend/app/services/__init__.py rename to diligence/utils/__init__.py diff --git a/backend/app/utils/auth.py b/diligence/utils/auth.py similarity index 95% rename from backend/app/utils/auth.py rename to diligence/utils/auth.py index 65bcbf8..6380a2b 100644 --- a/backend/app/utils/auth.py +++ b/diligence/utils/auth.py @@ -9,8 +9,8 @@ from fastapi.security import OAuth2PasswordBearer from jose import JWTError, jwt from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession -from app.config import get_settings -from app.database import get_db +from diligence.config import get_settings +from diligence.database import get_db settings = get_settings() ALGORITHM = "HS256" # Hardcoded — not configurable for security @@ -44,7 +44,7 @@ async def get_current_user( if not token: raise credentials_exception - from app.models.user import User + from diligence.models.user import User # Check if this is an API token (MCP connector auth) if settings.api_token and token == settings.api_token: diff --git a/backend/app/utils/dates.py b/diligence/utils/dates.py similarity index 100% rename from backend/app/utils/dates.py rename to diligence/utils/dates.py diff --git a/docker-compose.yml b/docker-compose.yml index 54633e6..e39adf8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,10 +15,13 @@ services: start_period: 10s backend: - build: ./backend + build: + context: . + dockerfile: backend/Dockerfile restart: unless-stopped env_file: .env environment: + - DATABASE_URL=postgresql+asyncpg://fitness@fitness-db:5432/fitness_rewards - BASE_URL=${BASE_URL:-http://localhost} - API_TOKEN=${API_TOKEN:-} depends_on: diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..b9990c0 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,65 @@ +[build-system] +requires = ["setuptools>=68.0", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "diligence" +version = "2.0.0" +description = "Self-hosted fitness rewards platform with AI agent integration" +readme = "README.md" +license = {text = "MIT"} +requires-python = ">=3.11" +authors = [ + {name = "DiligenceWorks Pte. Ltd.", email = "hello@diligenceworks.online"}, +] +keywords = ["fitness", "self-hosted", "mcp", "ai-agent", "rewards"] +classifiers = [ + "Development Status :: 4 - Beta", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", +] + +dependencies = [ + "fastapi>=0.115.0", + "uvicorn[standard]>=0.30.0", + "sqlalchemy[asyncio]>=2.0.30", + "aiosqlite>=0.20.0", + "pydantic>=2.10.0", + "pydantic-settings>=2.7.0", + "python-jose[cryptography]>=3.3.0", + "bcrypt>=4.2.0", + "httpx>=0.27.0", + "python-multipart>=0.0.18", +] + +[project.optional-dependencies] +postgres = [ + "asyncpg>=0.30.0", +] +mcp = [ + "mcp[cli]>=1.0.0", +] +dev = [ + "pytest>=8.0", + "pytest-asyncio>=0.23", + "ruff>=0.8.0", +] + +[project.scripts] +diligence = "diligence.cli:main" + +[project.urls] +Homepage = "https://github.com/DiligenceWorks/Diligence" +Documentation = "https://github.com/DiligenceWorks/Diligence#readme" +Repository = "https://github.com/DiligenceWorks/Diligence" +Issues = "https://github.com/DiligenceWorks/Diligence/issues" + +[tool.setuptools.packages.find] +include = ["diligence*"] + +[tool.setuptools.package-data] +diligence = ["frontend/**/*"] diff --git a/setup.ps1 b/setup.ps1 index 3cd52da..1ccb407 100644 --- a/setup.ps1 +++ b/setup.ps1 @@ -1,32 +1,71 @@ -# Diligence — Windows Setup +# Diligence Setup — Windows +Write-Host "" +Write-Host " Diligence Setup" +Write-Host "" -Write-Host "`n`e[36m💪 Diligence — Setup`e[0m`n" +function New-RandomHex { -join ((1..32) | ForEach-Object { '{0:x2}' -f (Get-Random -Max 256) }) } -if (Test-Path .env) { - Write-Host "`e[33m.env already exists. Delete it first to regenerate.`e[0m" - exit 1 +$Secret = New-RandomHex +$Token = New-RandomHex + +# Detect mode — Docker if docker compose is available and docker-compose.yml exists +$DockerAvailable = $false +try { docker compose version 2>$null | Out-Null; $DockerAvailable = $true } catch {} + +if ($DockerAvailable -and (Test-Path "docker-compose.yml")) { + Write-Host " Mode: Docker (PostgreSQL)" + Write-Host "" + + if (Test-Path ".env") { + Write-Host " .env already exists. Delete it first to regenerate." + exit 1 + } + + Copy-Item ".env.example" ".env" + (Get-Content ".env") -replace '^SECRET_KEY=.*', "SECRET_KEY=$Secret" ` + -replace '^API_TOKEN=.*', "API_TOKEN=$Token" ` + -replace '^DATABASE_URL=.*', 'DATABASE_URL=postgresql+asyncpg://fitness@fitness-db:5432/fitness_rewards' ` + -replace '^BASE_URL=.*', 'BASE_URL=http://localhost' | + Set-Content ".env" + + Write-Host " .env created" + Write-Host "" + Write-Host " Next steps:" + Write-Host " docker compose up -d" + Write-Host " Open http://localhost" + +} else { + Write-Host " Mode: Local (SQLite)" + Write-Host "" + + $DataDir = Join-Path $env:USERPROFILE ".diligence" + if (-not (Test-Path $DataDir)) { New-Item -ItemType Directory -Path $DataDir | Out-Null } + $EnvFile = Join-Path $DataDir ".env" + + if (Test-Path $EnvFile) { + Write-Host " Config already exists at $EnvFile" + Write-Host " Delete it to regenerate." + exit 1 + } + + @" +SECRET_KEY=$Secret +API_TOKEN=$Token +BASE_URL=http://localhost:8000 +DATA_DIR=$DataDir +"@ | Set-Content $EnvFile + + Write-Host " Config created at $EnvFile" + Write-Host "" + Write-Host " Next steps:" + Write-Host " pip install ." + Write-Host " diligence" + Write-Host "" + Write-Host " Data stored in: $DataDir" } -# Generate random SECRET_KEY (64 hex chars) -$bytes = New-Object byte[] 32 -[System.Security.Cryptography.RandomNumberGenerator]::Fill($bytes) -$secret = ($bytes | ForEach-Object { $_.ToString("x2") }) -join "" - -# Generate random API_TOKEN (32 alphanumeric chars) -$apiToken = -join ((48..57) + (65..90) + (97..122) | Get-Random -Count 32 | ForEach-Object {[char]$_}) - -# Create .env from template and replace both keys -Copy-Item .env.example .env -(Get-Content .env) -replace "^SECRET_KEY=.*", "SECRET_KEY=$secret" -replace "^API_TOKEN=.*", "API_TOKEN=$apiToken" | Set-Content .env - -Write-Host "`e[32m✅ .env created with random SECRET_KEY and API_TOKEN`e[0m" Write-Host "" -Write-Host "Next steps:" -Write-Host " docker compose up -d" -Write-Host " Open http://localhost" -Write-Host " Register your account" -Write-Host " Configure integrations via Settings or your AI agent" +Write-Host " MCP agent connection:" +Write-Host " URL: http://localhost:3001/sse" +Write-Host " Header: Authorization: Bearer $Token" Write-Host "" -Write-Host "MCP agent connection:" -Write-Host " URL: http://localhost:3001/sse" -Write-Host " Header: Authorization: Bearer $apiToken" diff --git a/setup.sh b/setup.sh index 1118d35..ef02031 100755 --- a/setup.sh +++ b/setup.sh @@ -1,26 +1,81 @@ #!/bin/bash -echo "💪 Diligence — Setup" +set -e +echo "" +echo " Diligence Setup" +echo "" -if [ -f .env ]; then - echo ".env already exists. Delete it first to regenerate." - exit 1 +# Detect install mode +if command -v docker compose &> /dev/null && [ -f docker-compose.yml ]; then + MODE="docker" +else + MODE="local" fi -SECRET=$(openssl rand -hex 32) -TOKEN=$(openssl rand -hex 32) -cp .env.example .env -sed -i.bak "s/^SECRET_KEY=.*/SECRET_KEY=${SECRET}/" .env -sed -i.bak "s/^API_TOKEN=.*/API_TOKEN=${TOKEN}/" .env -rm -f .env.bak +# Generate secrets +SECRET=$(openssl rand -hex 32 2>/dev/null || python3 -c "import secrets; print(secrets.token_hex(32))") +TOKEN=$(openssl rand -hex 32 2>/dev/null || python3 -c "import secrets; print(secrets.token_hex(32))") + +if [ "$MODE" = "docker" ]; then + echo " Mode: Docker (PostgreSQL)" + echo "" + + if [ -f .env ]; then + echo " .env already exists. Delete it first to regenerate." + exit 1 + fi + + cp .env.example .env + sed -i.bak "s/^SECRET_KEY=.*/SECRET_KEY=${SECRET}/" .env + sed -i.bak "s/^API_TOKEN=.*/API_TOKEN=${TOKEN}/" .env + sed -i.bak "s|^DATABASE_URL=.*|DATABASE_URL=postgresql+asyncpg://fitness@fitness-db:5432/fitness_rewards|" .env + sed -i.bak "s|^BASE_URL=.*|BASE_URL=http://localhost|" .env + rm -f .env.bak + + echo " .env created" + echo "" + echo " Next steps:" + echo " docker compose up -d" + echo " Open http://localhost" + echo "" + echo " MCP agent connection:" + echo " URL: http://localhost:3001/sse" + echo " Header: Authorization: Bearer ${TOKEN}" + +else + echo " Mode: Local (SQLite)" + echo "" + + DATA_DIR="${HOME}/.diligence" + mkdir -p "$DATA_DIR" + ENV_FILE="${DATA_DIR}/.env" + + if [ -f "$ENV_FILE" ]; then + echo " Config already exists at ${ENV_FILE}" + echo " Delete it to regenerate." + exit 1 + fi + + cat > "$ENV_FILE" << EOF +SECRET_KEY=${SECRET} +API_TOKEN=${TOKEN} +BASE_URL=http://localhost:8000 +DATA_DIR=${DATA_DIR} +EOF + + echo " Config created at ${ENV_FILE}" + echo "" + echo " Next steps:" + echo " pip install ." + echo " diligence" + echo "" + echo " Or run directly:" + echo " python -m diligence" + echo "" + echo " Data stored in: ${DATA_DIR}" + echo "" + echo " MCP agent connection:" + echo " URL: http://localhost:3001/sse" + echo " Header: Authorization: Bearer ${TOKEN}" +fi -echo "✅ .env created with random SECRET_KEY and API_TOKEN" echo "" -echo "Next steps:" -echo " docker compose up -d" -echo " Open http://localhost" -echo " Register your account" -echo " Configure integrations via Settings or your AI agent" -echo "" -echo "MCP agent connection:" -echo " URL: http://localhost:3001/sse" -echo " Header: Authorization: Bearer ${TOKEN}"