# --- Server --- # BACKEND_PORT takes precedence over PORT. Prefer it: many tools inject PORT # for the frontend, and Flask would otherwise take the React dev server's port. BACKEND_PORT=5000 # --- Database: sqlite (default) or mariadb --- DB_TYPE=sqlite # SQLite file (used when DB_TYPE=sqlite) DATABASE_PATH=./data/health.db # MariaDB (used when DB_TYPE=mariadb) — production DB is co-located with the # app on the Oracle box (129.146.26.249, MariaDB 10.3.39), TCP 127.0.0.1:3306, # dedicated account (not root). MariaDB treats `user@localhost` and # `user@127.0.0.1` as two different accounts — if you create this user by # hand, create both host variants with the same password, or TCP connections # fail with a password that looks right but is not the one that account has. # MARIADB_HOST=127.0.0.1 # MARIADB_PORT=3306 # MARIADB_USER=garmin # MARIADB_PASSWORD=your_production_mariadb_password # MARIADB_DATABASE=garmin_health_lab # --- Auth --- # CHANGE THIS in production! Used to sign JWTs (7-day expiry by default). JWT_SECRET=dev_secret_change_me JWT_EXPIRY_DAYS=7 # --- auth-hub OAuth2 provider (centralized SSO) --- # See docs/AUTH_HUB_INTEGRATION.md for setup instructions. # # Base URL of the auth-hub service AUTH_HUB_BASE_URL=http://129.146.26.249:5300 # # OAuth2 client credentials (obtain from auth-hub.manage_clients create) # NOTE: these must be registered against the auth-hub instance AUTH_HUB_BASE_URL # actually points to (dev vs prod are separate databases with separate clients). # Put the REAL values in backend/.env (gitignored) — never here. AUTH_HUB_CLIENT_ID=your_client_id AUTH_HUB_CLIENT_SECRET=your_client_secret # # Callback URL (must exactly match what's registered in auth-hub). Production # registers https://garmin.zichuan.xyz/auth/callback — add/remove redirect # URIs on that client with /opt/auth-hub's manage_clients CLI, not by editing # auth-hub's own database directly. AUTH_HUB_REDIRECT_URI=http://127.0.0.1:5500/auth/callback # --- CORS (comma-separated allowed front-end origins) --- # localhost stays in the production list on purpose: CORS is not an auth # boundary — every data route requires a valid JWT — so allowing a developer's # dev server costs nothing and saves toggling this on every session. CORS_ORIGIN=http://localhost:3000,http://localhost:5173 # --- AI models (text-only, large context) --- # Put REAL keys in backend/.env — that file is gitignored. Never commit keys. # Any model whose credentials are absent is skipped automatically. # Self-hosted AI gateway (model id "gateway"). OpenAI-compatible; it fans out # over nvidia/gemini/ollama itself and rotates several Gemini keys, so it # absorbs single-vendor quota limits. Reached directly, bypassing any local # HTTP proxy. NOTE: its NVIDIA upstream is a large reasoning model — replies # can take 2-3 minutes, so set AI_TIMEOUT_SECONDS accordingly. # Production (co-located with ai-gateway on the same Oracle box) uses the # loopback address — skips Caddy and the public hop entirely, and the token # never leaves localhost either way. Use the HTTPS domain instead only when # this app runs somewhere else: the token rides in an Authorization header # and must not cross the public internet in the clear. AI_GATEWAY_BASE_URL=http://127.0.0.1:5100/v1 AI_GATEWAY_TOKEN= AI_GATEWAY_MODEL=ai-gateway-auto # Google AI Studio -> "gemini-flash". Free-tier quota is small; 429s are common. GEMINI_API_KEY= # NVIDIA NIM -> "llama-70b", "nemotron-49b", "mistral-large". # Model ids come from that account's live GET /v1/models — do not guess them. NVIDIA_API_KEY= # NVIDIA_BASE_URL=https://integrate.api.nvidia.com/v1 # Preference order. The first configured model answers; if it fails or times # out, the next is tried. Read per request, so changes need no restart. AI_MODEL_CHAIN=gateway,gemini-flash,llama-70b # Max days of history sent (CSV-encoded). Trimmed further per model so the # payload always fits that model's own context window. AI_DAY_BUDGET=365 # Measured against the gateway, not guessed: a trivial prompt took 138s end to # end, because its primary upstream emits a full chain of thought before the # answer. Nothing user-facing blocks on this (the briefing generates in a # background thread), but the timeout still has to clear the real latency. AI_TIMEOUT_SECONDS=300 # Output cap. Reasoning models spend part of it thinking before they answer; # entries that need more declare their own budget in services/ai.py. AI_MAX_TOKENS=1024 # Output cap for the AI coach (晨报 / 趋势归因 / Copilot). Larger than # AI_MAX_TOKENS above: the same reasoning trace is spent from this budget # before the answer starts, and at 1024 the reply was all thinking with the # JSON truncated away. AI_COACH_MAX_TOKENS=4000 # --- AI coach job queue --- # Three projects share the gateway, so going too high causes 502s. AI_JOB_CONCURRENCY=2 AI_JOB_GAP_SECONDS=5 # Set AI_JOBS=false to stop consuming entirely (screens then show the computed # figures with no model reading). AI_JOBS=true # --- Garmin 请求节流 --- # 一天的历史要 9 次 API 调用(_extract_daily 6 + daily_extras 3),短同步再加 # 5 次曲线,每条没有详情的运动 1 次。730 天全历史 ≈ 6600 个请求。以前是能发多 # 快发多快。 # # 0.5 秒的依据(2026-09-03 调研,见 services/garmin_throttle.py 顶部注释): # sirredbeard/garmin-data-export 默认 0.15s、evg656e/garmin-connect-scraper # 默认 3s、佳明官方合作方 API 是 100 次/分钟(合 0.6s)。 GARMIN_MIN_INTERVAL_SECONDS=0.5 # 单次同步的请求预算。跑满就干净收尾,下次接着跑(已存的天数会跳过)。 # 1200 × 0.5s ≈ 10 分钟,够覆盖四个月历史。 GARMIN_REQUEST_BUDGET=1200 # 每次同步补多少条运动详情。新账号有几百条,不限量就是一次性打光预算。 GARMIN_DETAILS_PER_SYNC=40