{"owner":"CodeWithCJ","repo":"SparkyFitness","hasSkills":true,"totalSkillsCount":13,"totalTokensCount":35126,"categories":["anthropic-skill","copilot-instructions","root-instruction","claude-rule"],"hasMcp":false,"mcpConfig":null,"found":[".claude/skills/new-migration/SKILL.md",".github/copilot-instructions.md","AGENTS.md","docs/CLAUDE.md","shared/AGENTS.md","shared/CLAUDE.md","SparkyFitnessFrontend/CLAUDE.md","SparkyFitnessFrontend/AGENTS.md","SparkyFitnessMobile/AGENTS.md","SparkyFitnessServer/AGENTS.md","SparkyFitnessMobile/CLAUDE.md","CLAUDE.md","SparkyFitnessServer/CLAUDE.md"],"skills":{".claude/skills/new-migration/SKILL.md":"---\nname: new-migration\ndescription: Use whenever adding or changing a SparkyFitness database migration, creating a new table, or altering user-visible data access. Walks the mandatory cross-package checklist (RLS policies, schema backup sync, shared Zod schema, docs security tiers, downstream contracts). Triggers on \"new table\", \"migration\", \"ALTER TABLE\", \"RLS\", \"schema change\".\n---\n\n# New Migration\n\nSee @.agents/skills/new-migration/SKILL.md — that is the canonical skill body. Read it and follow it.\n",".github/copilot-instructions.md":"# SparkyFitness AI Coding Agent Instructions\n\n## Architecture Overview\n\n**Full-stack fitness tracking application** - self-hosted alternative to MyFitnessPal with AI-powered nutrition assistance.\n\n- **Frontend**: React 18 + TypeScript + Vite (`src/`)\n- **Backend**: Node.js/Express (`SparkyFitnessServer/`)\n- **Database**: PostgreSQL with Row Level Security (RLS)\n- **UI**: Tailwind CSS + shadcn/ui components\n- **State**: React Context + TanStack Query\n- **AI**: Multi-provider support (OpenAI, Anthropic, Google, etc.)\n\n## Development Workflow\n\n### Running the Application\n```bash\n# Frontend development server (port 8080)\nnpm run dev\n\n# Backend server (port 3010)  \nnpm run start-backend\n\n# Docker development (local builds)\n./docker/docker-helper.sh dev up\n\n# Docker production (DockerHub images)\n./docker/docker-helper.sh prod up\n```\n\n### Key Configuration Files\n- **Vite config** (`vite.config.ts`): API proxy setup (`/api` → `localhost:3010`)\n- **Environment**: Root `.env` file (loaded by backend at `path.resolve(__dirname, '../.env')`)\n- **Docker**: `docker/` directory contains organized Docker configuration\n  - `docker-compose.dev.yml` - Local development builds with volume mounts\n  - `docker-compose.prod.yml` - Production DockerHub images\n  - `docker-helper.sh` - Comprehensive management script with help system\n  - `nginx.conf.template` - Parameterized nginx configuration with envsubst\n  - `.env.example` - Template for environment variables\n\n### Docker Helper Script\nThe `docker-helper.sh` script provides easy management:\n```bash\n# Show all available commands\n./docker/docker-helper.sh help\n\n# Development environment\n./docker/docker-helper.sh dev up     # Start all services\n./docker/docker-helper.sh dev logs   # View logs\n./docker/docker-helper.sh dev clean  # Clean volumes and images\n\n# Production environment  \n./docker/docker-helper.sh prod up    # Start production services\n./docker/docker-helper.sh prod down  # Stop and remove containers\n```\n\n## Critical Architecture Patterns\n\n### Repository Pattern (Backend)\nAll database operations use repository pattern in `SparkyFitnessServer/models/`:\n```javascript\n// Example: userRepository.js\nconst pool = require('../db/connection');\n\nasync function createUser(userId, email, hashedPassword, full_name) {\n  const client = await pool.connect();\n  try {\n    await client.query('BEGIN');\n    // Multiple related inserts in transaction\n    await client.query('COMMIT');\n  } catch (error) {\n    await client.query('ROLLBACK');\n    throw error;\n  } finally {\n    client.release();\n  }\n}\n```\n\n### External Provider Integration\nModular integration system in `SparkyFitnessServer/integrations/`:\n- **Food providers**: OpenFoodFacts, Nutritionix, FatSecret\n- **Exercise data**: Wger integration\n- **Health data**: Apple Health integration\n- **Encryption**: All API keys encrypted at rest using `security/encryption.js`\n\n### Frontend Context Providers\nKey contexts in `src/contexts/`:\n- **PreferencesContext**: User settings, goals, theme, AI provider config\n- **ChatbotVisibilityContext**: AI assistant state management\n\n### Database Schema Conventions\n- **UUID primary keys** for all tables (`gen_random_uuid()`)\n- **Audit fields**: `created_at`, `updated_at` on all tables\n- **RLS policies**: Users only access their own data\n- **Transaction patterns**: Multi-table operations wrapped in transactions\n\n## AI Integration Patterns\n\n### Multi-Provider AI Support\n```javascript\n// SparkyFitnessServer/ai/config.js\nfunction getDefaultModel(serviceType) {\n  switch (serviceType) {\n    case 'openai': return 'gpt-4o-mini';\n    case 'anthropic': return 'claude-3-5-sonnet-20241022';\n    case 'google': return 'gemini-pro';\n    // ...\n  }\n}\n```\n\n### Chat System Architecture\n- **Backend**: `routes/chatRoutes.js` handles AI provider routing\n- **Frontend**: `DraggableChatbotButton` component for UI\n- **Storage**: Chat history with metadata in PostgreSQL\n- **Capabilities**: Food logging, image analysis, exercise tracking, measurements\n\n## Data Flow Patterns\n\n### Meal Planning System\n1. **Templates** (`meal_plan_templates`) → **Scheduled Plans** (`meal_plans`) → **Food Diary Entries** (`food_diary`)\n2. Complex meal expansion logic in `mealPlanTemplateRepository.js`\n3. Date-range planning support with single-day granularity\n\n### Family Access Control\n- **Granular permissions**: `calorie`, `checkin`, `reports`, `food_list`\n- **RLS enforcement**: Database-level security policies\n- **Access patterns**: `familyAccessRepository.js` manages relationship permissions\n\n### External API Error Handling\nConsistent error patterns across integrations:\n```javascript\ntry {\n  const response = await fetch(searchUrl, { method: 'GET' });\n  if (!response.ok) {\n    const errorText = await response.text();\n    log('error', \"API error:\", errorText);\n    throw new Error(`API error: ${errorText}`);\n  }\n  return await response.json();\n} catch (error) {\n  log('error', `Error with query \"${query}\":`, error);\n  throw error;\n}\n```\n\n## Development Conventions\n\n### API Response Patterns\n- **Success**: JSON data directly\n- **Error**: `{ error: \"message\" }` with appropriate HTTP status\n- **Auth**: JWT tokens in Authorization header\n- **CORS**: Configured for frontend URL in environment\n\n### Database Migration Strategy\n- **Auto-migrations**: `utils/dbMigrations.js` runs on server startup\n- **Version tracking**: Migrations applied based on version numbers\n- **Schema changes**: Always backward compatible during development\n\n### Component Architecture\n- **shadcn/ui** for base components\n- **Custom components** in `src/components/`\n- **Page-level** components in `src/pages/`\n- **Shared utilities** in `src/utils/`\n\n### Environment-Specific Behavior\n- **Development**: Vite proxy, hot reload, debug logging\n- **Docker**: Production builds, environment variable injection\n- **Database**: Connection pooling, transaction management\n\n## Key Files for Understanding\n- **Server entry**: `SparkyFitnessServer/SparkyFitnessServer.js` (route registration, middleware setup)\n- **Frontend entry**: `src/App.tsx` (context providers, query client setup)\n- **Documentation site**: `docs/` - Comprehensive Nuxt Content documentation (https://codewithcj.github.io/SparkyFitness)\n\n## Documentation Structure\n\n### Developer Documentation\nThe developer documentation has been consolidated into focused guides at `docs/content/3.developer/`:\n\n1. **`1.getting-started.md`** - Complete setup, installation, and quick start guide\n2. **`2.architecture.md`** - System architecture, patterns, and project structure  \n3. **`3.contributing.md`** - Development workflow, guidelines, and PR process\n4. **`4.database.md`** - Database schema, migrations, and operations\n5. **`5.api-reference.md`** - OpenAPI specification and endpoint documentation\n6. **`6.troubleshooting.md`** - Comprehensive debugging guide and common solutions\n\n### Documentation Site Features\n- **Live site**: https://codewithcj.github.io/SparkyFitness hosted on GitHub Pages\n- **Technology**: Nuxt Content + Docus theme for optimal developer experience\n- **Navigation**: Auto-generated navigation with numerical ordering\n- **Search**: Full-text search across all documentation\n- **Mobile-friendly**: Responsive design for all devices\n","AGENTS.md":"# AGENTS.md\n\n*Last updated: 2026-10-22*\n\nThis is the repo-root monorepo guide for SparkyFitness. Use it to choose the right package, understand shared repo-level rules, and find the next guide to read.\n\n**For AI Tools & Developers:** Start with `agent-docs/README.md` (in this repo) for quick navigation to:\n- `file-and-domain-reference.md` — Find any code by feature in seconds\n- `testing-patterns.md` — Concrete test examples for each layer\n- `architecture-permissions.md` — Permission types and RLS patterns\n- Plus 5 more guides for migrations, data flow, anti-patterns, new features, and planning\n\nPackage-level guides win. For work inside a package, follow that package's `AGENTS.md` when present, otherwise its `CLAUDE.md`.\n\n## Scope\n\n- Start here when work begins at repo root or spans multiple packages.\n- Keep root-level guidance focused on workspace layout, shared conventions, and cross-package coordination.\n- Root `package.json` is tooling only (`husky`, `lint-staged`, `prettier`), not an app entrypoint.\n- Run scripts from the package directory you are changing.\n\n## Package Guides\n\n- Repo-root alias: `CLAUDE.md` points to this file.\n- Frontend: `SparkyFitnessFrontend/AGENTS.md`\n- Server: `SparkyFitnessServer/AGENTS.md`\n- Mobile: `SparkyFitnessMobile/AGENTS.md`\n- Shared: `shared/AGENTS.md`\n\nFor `docs/` and `SparkyFitnessGarmin/`, there is no package-level `AGENTS.md`. `SparkyFitnessGarmin/` is only a handful of Python files (`main.py`, `routes.py`, `service.py`, `schemas.py`); read them directly. For `docs/`, inspect the local manifest and content layout.\n\n## Monorepo Map\n\n- `SparkyFitnessFrontend/` - React 19 + Vite web app.\n- `SparkyFitnessServer/` - Express 5 + PostgreSQL backend API.\n- `SparkyFitnessMobile/` - Expo SDK 57 / React Native 0.86 app.\n- `shared/` - source-first TypeScript workspace package for `@workspace/shared` schemas, constants, and timezone/day helpers.\n- `docs/` - VitePress documentation site.\n- `docs/readme/` - translated copies of the root `README.md` (`README_<lang>.md`), outside the VitePress `srcDir`. The language bar at the top of `README.md` links to them; when the root README changes substantially, the translations go stale.\n- `SparkyFitnessGarmin/` - standalone Python integration service outside the current `pnpm` workspace.\n- `docker/`, `helm/`, `.github/` - infra and deployment assets.\n- `umbrel/sparkyfitness/` - Umbrel App Store package (manifest, compose, derived secrets), submitted to `getumbrel/umbrel-apps`. See `umbrel/README.md`.\n- `db_schema_backup.sql` - repo-root schema snapshot kept in sync by CI (`.github/workflows/schema-backup.yml`); never hand-edit or regenerate locally.\n- `docker/.env.example` - tracked env template commonly copied to repo-root `.env`.\n\n## Workspace Notes\n\n- `pnpm-workspace.yaml` currently lists `frontend`, `SparkyFitnessFrontend`, `shared`, `SparkyFitnessMobile`, `SparkyFitnessServer`, and `docs`.\n- Only `SparkyFitnessFrontend/` exists on disk right now; treat `frontend` as a legacy workspace entry unless the task is specifically about workspace cleanup.\n- `shared/` is a library package, not an app; its `pnpm run validate` runs typecheck, a hardcoded-string audit, and the Prettier check. CI runs that in a `Shared Validate` job and also re-runs the frontend, mobile, and server jobs whenever `shared/` changes, so validate shared changes from the consuming package(s), not in isolation.\n- `SparkyFitnessGarmin/` is outside the current workspace. Inspect its own manifest and scripts before working there.\n\n## Agent Efficiency (read this before searching)\n\nDo not read or search these paths; they burn context for nothing:\n\n- `WIP/` - personal scratch area; contains zips and full copies of other repos, including a stale duplicate of this repo (`WIP/SparkyFitness-main/`). Never read or edit anything under it.\n- `SparkyFitnessMobile/ios/` and `SparkyFitnessMobile/android/` - generated native projects (`ios/` is >1 GB of Pods). Regenerate with `npx expo prebuild --clean`; edit `app.config.ts`, `plugins/`, or `targets/` instead.\n- `pnpm-lock.yaml` (~1.3 MB) - never read; check `package.json` files instead.\n- `db_schema_backup.sql` (~330 KB) - never read whole; grep for the one `CREATE TABLE` you need.\n- `SparkyFitnessFrontend/dist/` - build output.\n- `SparkyFitnessFrontend/public/locales/` except `en/` - 35 machine-synced translations. Only `en/translation.json` is ever hand-edited, and even that (~120 KB) should be grepped, not read whole.\n\nCheap ways to learn things:\n\n- Database table index: read `docs/src/developer/database.md` (quick reference of all ~120 tables with one-line purpose). For detailed schema, read `shared/src/schemas/database/<Table>.zod.ts` (one small Zod file per table).\n- Database security & permissions: `docs/src/developer/database-security-tiers.md` (security tier, permission type, and RLS rules for every table).\n- API request/response contract: `shared/src/schemas/api/<Name>.api.zod.ts`.\n- Definition of done: CI (`.github/workflows/ci-tests.yml`) runs `pnpm run validate` (which includes Knip unused export & dead code checks in frontend and mobile) plus the package's CI test script for each changed package. Run those locally before declaring work complete.\n\n## Cross-Package Rules\n\n- If you add or change a server migration (such as creating a new table), follow `agent-docs/new-migration-checklist.md`. In short, you MUST:\n  1. Create the migration file in `SparkyFitnessServer/db/migrations/YYYYMMDDHHMMSS_description.sql`.\n  2. Update the Row-Level Security (RLS) policies in `SparkyFitnessServer/db/rls_policies.sql`.\n  3. **Restart the server** (`pnpm start` from `SparkyFitnessServer/`) to apply the migration.\n  4. Leave `db_schema_backup.sql` alone — after merge, CI regenerates it from the migrations and opens an automated sync PR (`.github/workflows/schema-backup.yml`). Never manually edit the backup file or commit a locally generated copy.\n  5. Add or update the matching Zod schema in `shared/src/schemas/database/`.\n  6. Update the user-facing documentation in `docs/src/features/family-friends-sharing.md`.\n  7. Update the developer documentation in `docs/src/developer/database-security-tiers.md` to classify the table as Tier 1, Tier 2, or Tier 3.\n- **Heart rate zones have one source of truth:** `shared/src/utils/heartRateZones.ts` (`resolveHeartRateZones`, `heartRateZoneRange`, `computeTimeInZones`). Never re-derive max heart rate or zone floors in a package; server stored zones, training load, mobile suggested ranges and the web/mobile charts and settings all call it. Max heart rate is the person's own value, else Nes (211 - 0.64 x age) from date of birth, else 190. Settings never rewrite stored `exercise_entry_hr_zones` rows. iOS 27 can fill custom floors from Apple Health (`floorsFromAppleHeartRateZones`); that does not change max heart rate. A finished Apple workout's own zone group (`bucketsFromAppleWorkoutZones`) is stored as `device` zones instead of recomputing, so the chart matches Apple, including time below the first boundary.\n- Prefer the shared timezone helpers from `@workspace/shared` and `SparkyFitnessServer/utils/timezoneLoader.ts` for day-string logic. Avoid `toISOString().split('T')[0]` for user-facing or business-logic dates.\n- Keep `YYYY-MM-DD` values as calendar-day strings until you reach a database or external API boundary that needs UTC instants.\n- Auth or API contract changes usually need a quick check in both web and mobile because they share the same backend.\n- Frontend local dev proxies `/api`, `/health-data`, and `/uploads` to the server on `3010`. The `/health-data` proxy is rewritten to `/api/health-data`, while server APIs remain rooted at `/api`.\n- Server runtime secrets are usually sourced from repo-root `.env`, commonly created from `docker/.env.example`. The server can also load secret files via `SparkyFitnessServer/utils/secretLoader.ts`.\n- Extract shared logic on the **second** duplication (\"rule of two\"), not the third - duplicated logic drifts as different sessions edit each copy. Extract *behavior*, not coincidental shape. See `agent-docs/anti-patterns.md`.\n- **Strict TypeScript Typing:** Never use `any` or `// eslint-disable-next-line @typescript-eslint/no-explicit-any` when creating new functions or editing existing code. When modifying existing files, proactively update legacy `any` declarations in the touched code to reflect their proper TypeScript data types, interfaces, or schemas from `@workspace/shared`. Do NOT copy legacy `any` parameter signatures forward when refactoring or extending service/repository files.\n- **Library Deletes vs Diary Snapshots:** `exercise_entries` and `food_entries` are self-contained snapshots, not pointers (`exercise_id` and `food_id` are `ON DELETE SET NULL`). Deleting an exercise or food from the library (`mode: 'delete'`) preserves past and current diary history, cascades from presets and plan templates, cleans up future scheduled workout plan entries (`entry_date >= today`), and cleans up empty parent preset entries. Only explicit `delete_with_history` (force delete) purges diary logs for that user. If an item is referenced by other users (`otherUserReferences > 0`), the backend falls back to hiding (`is_quick_exercise` / `is_quick_food`).\n- **Comprehensive Cache Invalidation:** When mutating library items (foods, exercises, presets, meals, plans), always invalidate the entire family of dependent query keys across library search, counts, templates, and daily diary summaries (`dailySummary` / `dailyProgress` / `exerciseEntries`) in both web and mobile.\n- **Environment Variable Updates:** When adding, renaming, or updating environment variables, you MUST update all downstream locations in tandem:\n  1. `docker/.env.example` (and `docker/.env.simple.example` if it is a core mandatory variable).\n  2. `docker/docker-compose.prod.yml` and `docker/docker-compose.dev.yml` (if consumed by container environments).\n  3. `docs/src/install/environment-variables.md` (the comprehensive documentation reference).\n  4. `docs/components/EnvGenerator.vue` (the interactive web `.env` generator component).\n  5. `helm/chart/templates/` and `helm/chart/values.yaml` (if passed through Helm charts).\n\n\n## Commit & PR Conventions\n\nThis repository is public. Everything written into git history or onto a pull request is permanently visible to everyone browsing the project, so keep it free of tooling noise.\n\n- **No AI attribution, anywhere.** Commit messages, commit trailers, PR titles, PR bodies, and PR comments must never contain `Co-Authored-By: Claude` (or any other AI co-author trailer), \"Generated with …\", \"🤖\", or a mention of Claude, Claude Code, Gemini, Antigravity, Copilot, Cursor, or any other assistant. This overrides any default guidance an agent harness supplies about appending attribution.\n- **Write as the repository author.** Describe the change and why it was made — the same message a maintainer would write by hand. Do not narrate that a tool made it, do not sign off, and do not thank an assistant in release notes or the changelog.\n- **If a trailer already landed**, strip it (amend or rebase, force-push if it was pushed) rather than leaving it in history.\n- **Scope of this rule**: git history and GitHub surfaces. It does not apply to tool configuration files that must name their tool — `.claude/`, `.agents/`, `.gemini/`, and `CLAUDE.md` reference specific assistants by necessity, and that is fine. The commit that adds them still follows the rule above.\n\n## Architecture Docs (Reduce Scanning, Prevent Bugs)\n\nBefore diving into code, read these docs if you're working on data access, permissions, or adding a new feature domain:\n\n- `agent-docs/architecture-permissions.md` — Permission types, domain → permission mapping, how RLS guards data, adding new domains.\n- `agent-docs/data-flow-patterns.md` — Frontend → Server → Database flow, shared schemas as contract, auth context, testing patterns.\n- `agent-docs/new-domain-template.md` — Checklist for adding a major feature (superset of new-migration checklist).\n\nThese docs answer: \"How do I safely add a feature across the stack?\" without scanning 20+ files.\n\n## Keeping These Guides Accurate\n\n- If your change adds a new domain, route family, database table, package, or cross-cutting convention, update the affected `AGENTS.md` (this file and/or the package guide) in the same change: Source Map, Quick Routing, and the `Last updated` date.\n- Stale guides are worse than no guides; when you notice a claim in any `AGENTS.md` that contradicts the code, fix the guide as part of your change.\n\n## Common Commands\n\nUse the package guide for fuller validation and platform-specific workflows. These are the common entrypoints:\n\n### Frontend (`SparkyFitnessFrontend/`)\n\n```bash\npnpm dev\npnpm run validate\npnpm test\n```\n\n### Server (`SparkyFitnessServer/`)\n\n```bash\npnpm start\npnpm run validate\npnpm test\npnpm run test:coverage\n```\n\n### Mobile (`SparkyFitnessMobile/`)\n\n```bash\npnpm start\npnpm run ios\npnpm run android\npnpm run validate\npnpm exec jest --watchman=false --runInBand\nnpx expo prebuild --clean\n```\n\n### Docs (`docs/`)\n\n```bash\npnpm dev\npnpm run build\n```\n\n","docs/CLAUDE.md":"# CLAUDE.md\n\nThis is the SparkyFitness documentation site built with **VitePress**. For work on docs, inspect `package.json` scripts and the `src/` layout directly (`.vitepress/config.mts`).\n","shared/AGENTS.md":"# AGENTS.md\n\n_Last updated: 2026-10-22_\n\n`@workspace/shared` is a source-first TypeScript workspace library package for schemas, constants, and timezone/day helpers consumed by SparkyFitnessServer, SparkyFitnessFrontend, and SparkyFitnessMobile.\n\n## Scope\n\n- This package defines contracts and shared logic, not an app.\n- Validate changes from consuming packages (server, frontend, mobile), not in isolation.\n- Every schema change here potentially touches three packages.\n\n## Food Assistant State\n\n`schemas/api/FoodAssistant.api.zod.ts` and `schemas/database/FoodAssistant.zod.ts` define lasting preferences, versioned tasks, unresolved ingredients and idempotent operation records. All state is owner-only; these additive contracts do not change existing chat clients.\n\n## Structure\n\n- `src/schemas/database/` - one Zod file per table (`Foods.zod.ts`, `Exercises.zod.ts`, ~115 files). Agent shortcut: to learn a table shape, read the matching file here instead of the SQL dump.\n- `src/schemas/api/` - API request/response contracts (`*api.zod.ts`). `VoiceTranscription.api.zod.ts` defines multipart limits/MIME types, transcription responses, voice preference patches, and compatible public service options shared by web/mobile/server.\n- `src/constants/` - shared constants and enums (exercises, nutrients, meal types, fasting protocols, medication schedules, cycle phases, etc.).\n- `src/utils/` - timezone helpers (`todayInZone`, `instantToDay`, `instantForDay`, `dayToUtcRange`, `compareDays`, `addDays`, `isDayString`), cycle/menstruation helpers, unit/calculation utilities, the race plan generator (`racePlan.ts`, `buildRacePlan`), the run program schedule helpers (`runProgramSchedule.ts`, `runProgramCatchUp.ts`, `runProgramTimeline.ts`: `buildProgramTimeline` / `itemsByDay`, and `runProgramActions.ts`: the `PUT /api/v2/run-program` body builders; web and mobile both use them), and the training pace zones (`racePaceZones.ts`: `racePaceZones` turns a recent race result into easy, interval, tempo, marathon-pace and stride ranges with Riegel's formula; `equivalent5kPace` is the shared 5K conversion; `paceZones.ts` holds the effort-based `paceZones`, `thresholdPaceFromEffort` and `PACE_ZONE_IDS`), and the plan realigner (`runProgramRealign.ts` (`realignProgram` rebuilds the rest of a plan to end on the goal day: drops repeats and the shortest easy runs first, folds lighter weeks, never touches the taper or final workout, keeps two runs a week for three-plus run days, stretches with repeated base weeks when there is room, eases after a long gap, and reports an unrealistic goal with the options; `applyRealign` is the confirmed change, `toRealignResult` the API shape); its contract is in `RunProgram.api.zod.ts`: `realignRunProgramBodySchema`, `runProgramRealignResultSchema`).\n- `src/utils/heartRateZones.ts` - the single source of truth for heart rate zones: `resolveMaxHr` (own value, else Nes from date of birth, else observed max, else 190), `resolveHeartRateZones` (three methods: `percent_max`, `heart_rate_reserve` (Karvonen, needs resting HR) and `custom` floors; falls back to percent of max when the chosen method lacks input and says so in `effectiveMethod`), `heartRateZoneRange(resolved, zone)` (bpm range of zone 1-5; the top zone ends at max HR - the function for training programs that need \"zone N in bpm\"), `computeTimeInZones` (buckets a heart rate series), `percentMaxFloors` / `heartRateReserveFloors`, `floorsFromAppleHeartRateZones` (five custom floors from Apple's preferred zones; null unless there are exactly five, and a missing zone-1 minimum becomes 30 bpm), `bucketsFromAppleWorkoutZones` (a finished workout's five zone durations, keeping time below the first boundary as zone 1; empty when Apple reported no time, null when the group is not five ascending zones). `trainingLoad.ts` weights sessions by these zones. The API contract is `src/schemas/api/HeartRateZones.api.zod.ts`.\n- `src/nutrients/` - nutrient catalogs and the goal calculator used by web onboarding, the web Goals editor and mobile Goals (`nutrientCalculation.ts`: per-nutrient recommended values from age, sex, weight, calories and the saved algorithm choices; `nutrientAlgorithms.ts`: the algorithm enums and labels).\n- `src/ai/`, `src/cycle/`, `src/medications/`, `src/mood/` - domain-specific helpers. `src/medications/adherence.ts` is the one implementation of medication adherence for web and mobile (`computeAdherence`, `computeChecklistProgress`, `entryMatchesDue`, and the month-calendar helpers `tallyEntriesByDay`, `dayLogStatus`, `tallySymptomsByDay`); `subtype.ts` holds the All | Meds | Supplements filter rules. Both are pure, take timezone and data as arguments, and are tested in `SparkyFitnessServer/tests/medicationAdherence.test.ts` and `medicationSubtype.test.ts`.\n- `src/types/` - shared plain TypeScript types that are not Zod-derived.\n- Routes: `src/schemas/api/Route.api.zod.ts` (points, turn cues, bodies), `src/utils/routeGeometry.ts` (distance, climb, simplify, bounds, and the `localProjector` / `distanceToSegment` nearest-point helpers the route editor uses), `src/utils/routeGpx.ts` (dependency-free GPX read/write), `src/utils/routeGenerate.ts` (generate and follow-roads request builders, error kinds, turn counts) and `src/constants/routingProviderTypes.ts` (the `valhalla` external provider type and its default public server URL).\n- Routes: `src/schemas/api/Route.api.zod.ts` (points, turn cues, bodies), `src/utils/routeGeometry.ts` (distance, climb, simplify, bounds) and `src/utils/routeGpx.ts` (dependency-free GPX read/write).\n- `src/compare/` - compare any two tracked things: `stats.ts` (statistics), `metrics.ts` (metric catalog: kind, missing-day rule, weekly form, and `rolling` for a value read over the 7 days ending on the day), `analyze.ts` (day and week-by-week analysis), `since.ts` (before vs. since a habit started: suggested start, stretches, regularity, pre-trend; its limits are exported as `SINCE_*` constants), `question.ts` (the question model web and mobile share: timing choices, spans and `compareWindow`, custom-range limits, since-start bounds, `SUGGESTED_QUESTIONS`, and the recents helpers `knownCompareQuestions` / `withRecentQuestion`), `chartData.ts` (chart data both apps draw: `pairedDots` and `displayDots`, `trendLine`, `overTimeData`, `sinceChartData`). The API contract is `src/schemas/api/Compare.api.zod.ts`.\n- `src/symptoms/` - generic symptom tracking: constants and enums (scales, templates, sections, option kinds), template section resolution (`resolveSections`), built-in symptoms and pick-lists, head/body region ids for the location maps, and custom-field validation. The API contract is `src/schemas/api/Symptoms.api.zod.ts`.\n\n## Naming Convention\n\n- `X.api.zod.ts` = API request/response schema\n- `X.zod.ts` = database table schema\n- Export everything from `src/index.ts`; consuming packages import both types and values via `@workspace/shared`\n\n## Cross-Package Contract Rules\n\n- Changes to `src/schemas/api/` usually affect server routes and both frontend/mobile API clients.\n- Changes to `src/schemas/database/` require matching server migrations and RLS policies when changing the stored schema. CI regenerates the schema backup after merge; never hand-edit or regenerate it locally. Mirroring existing columns missing from a Zod schema does not require another migration.\n- Timezone/day-string helpers prevent bugs; prefer them over `toISOString().split('T')[0]`.\n- Verified commands (run from `shared/`): `pnpm run typecheck`, `pnpm run format:check`, `pnpm run format`, `pnpm run i18n:audit`, `pnpm run validate` (typecheck + i18n audit + format check). The package has no lint or tests of its own; behavior is covered by the consumers' test suites.\n- Shared exports ids and data; apps own translation. `pnpm run i18n:audit` (`scripts/audit-user-facing-strings.mjs`) fails when a file gains more prose in `label`/`title`/`description`/`message`/`hint`/`summary`/`body`/`text` properties than `scripts/user-facing-strings.baseline.json` allows (the cycle, GLP-1, and body-region English catalogs are grandfathered). After moving text out, run it with `--update-baseline` to lower the allowance; use `// i18n-ignore` for literals that must stay.\n- Formatting uses the same `.prettierrc` as the other packages, and lint-staged formats staged `shared/` files on commit.\n- CI (`.github/workflows/ci-tests.yml`) runs `Shared Validate` and re-runs the frontend, mobile, and server jobs on any `shared/**` change.\n- Test any shared change from the consumer packages (`pnpm run validate` in SparkyFitnessServer, SparkyFitnessFrontend, and SparkyFitnessMobile after modifying shared).\n\n## Working Rules\n\n- Keep this package export-focused and schema-focused; logic that scales should live in consuming packages.\n- Never export stale or unfinished types; if a consumer is drafting code and needs a type not yet here, add it.\n","shared/CLAUDE.md":"# CLAUDE.md\n\nSee @AGENTS.md\n","SparkyFitnessFrontend/CLAUDE.md":"# CLAUDE.md\n\nSee @AGENTS.md\n","SparkyFitnessFrontend/AGENTS.md":"# AGENTS.md\n\n_Last updated: 2026-10-23_\n\nSparkyFitness Frontend is the React web app for the SparkyFitness monorepo. Use this file as the primary guide for work inside `SparkyFitnessFrontend/`.\n\nIf a task also touches the server, mobile app, or `shared/`, read that package guide before editing outside this directory. Use `../AGENTS.md` for monorepo-level context.\n\n## Scope\n\n- This file is for package-local work in `SparkyFitnessFrontend/`.\n- `CLAUDE.md` just imports this file via `See @AGENTS.md`.\n- Run scripts from this directory.\n\n## Current Snapshot\n\n- Stack: React 19, Vite 8, TypeScript 6, Tailwind CSS v4 (via `@tailwindcss/vite`), shadcn/ui-style Radix primitives, TanStack Query 5, React Router 7 (`createBrowserRouter`), i18next, Better Auth client, Zod 4, Recharts.\n- `@/*` maps to `src/`; `@workspace/shared` maps to `../shared/src/index.ts` (also in Jest via `moduleNameMapper`).\n- Dev server runs on port `8080` and proxies `/api`, `/mcp`, and `/uploads` to the backend on `3010`; `/health-data` is proxied with an `/api` prefix rewrite. Override the backend host with `VITE_BACKEND_HOST`.\n- PWA (`vite-plugin-pwa`) is enabled in production builds only.\n\n## Verified Commands\n\n```bash\npnpm dev\npnpm run typecheck\npnpm run lint\npnpm run format:check\npnpm run format\npnpm run validate\npnpm test\npnpm run test:ci\npnpm run build\n```\n\n- `pnpm run validate` runs typecheck, lint (`--max-warnings 0`), Prettier check, and Knip (`pnpm run knip` for unused files and exports) together.\n- `pnpm test` runs Jest (`ts-jest`, `jsdom`); config is inline in `package.json`, setup in `src/tests/setupTests.ts`.\n- `pnpm run build` runs `validate` first, then `vite build`.\n- CI (`.github/workflows/ci-tests.yml`) runs `pnpm run validate` and `pnpm run test:ci` for this package when its files change; matching those locally means a green PR.\n\n## Domain-Mirrored Layout (the most important convention)\n\nFeatures are organized by domain, and the same domain folder name appears in `src/pages/`, `src/api/`, and `src/hooks/`. A feature change usually touches the matching folder in all three:\n\n- Page domains: `Admin`, `Auth`, `Chat`, `CheckIn`, `Cycle`, `Diary`, `Errors`, `Exercises`, `Fasting`, `Foods`, `Goals`, `Integrations`, `Medications`, `Reports`, `Settings`, `Symptoms`.\n- API domains add a few more: `AiConversions`, `Chatbot`, `Onboarding`, `Pregnancy`, `SleepScience`, `Symptoms`.\n- Example: a Medications bug lives in `src/pages/Medications/` + `src/api/Medications/` + `src/hooks/` medication hooks. Start there, not with a repo-wide search.\n\n## Source Map\n\n- Food assistant state: owner-only preferences, recoverable tasks and operation history use `FoodAssistant.api.zod.ts` / `FoodAssistant.zod.ts` in shared. Server: `routes/v2/foodAssistantRoutes.ts`, `services/foodAssistantService.ts`, `models/foodAssistantRepository.ts`, `ai/tools/foodAssistantTools.ts`. Web: `src/components/ai/FoodAssistantState.tsx`, `src/api/Chatbot/foodAssistantService.ts`.\n\n- `src/main.tsx` - app bootstrap; creates the shared `QueryClient` with global `QueryCache`/`MutationCache` handlers that render toasts from query/mutation `meta` (`errorTitle`, `errorMessage`, `successMessage`).\n- `src/App.tsx` - route registry via `createBrowserRouter`, plus `PrivateRoute` and `PermissionRoute` wrappers (permission-gated areas include `reports` and `admin`).\n- `src/pages/<Domain>/` - route screens by domain.\n- `src/pages/Reports/Compare/` - the Reports > Compare tab (`CompareReport` is the root, mounted by `Reports.tsx` outside its loading gate), with its data from `src/hooks/Reports/useCompare.ts` and `src/api/Reports/compareApi.ts`.\n- `src/api/api.ts` - `apiCall(endpoint, options)` helper: base URL `/api`, query `params`, JSON/FormData bodies, `responseType`, error toasts, `suppress404Toast`. Use it for all backend requests.\n- `src/api/<Domain>/` - per-domain API clients built on `apiCall`.\n- `src/hooks/<Domain>/` and `src/hooks/use*.ts(x)` - TanStack Query hooks and shared UI hooks (`use-toast`, `useDebounce`, `useAuth`, ...).\n- `src/components/` - shared components; `ui/` holds the shadcn-style primitives (~37 files); domain component folders include `Foods/`, `FoodSearch/`, `FoodUnitSelector/`, `Onboarding/`, `ExerciseCharts/`, `ai/` (assistant-ui chat pieces).\n- `src/contexts/` - `ActiveUserContext` (family-access acting-user switching), `PreferencesContext`, `ThemeContext`, `WaterContainerContext`, `ChatbotVisibilityContext`, `ChatToolCategoriesContext` (runtime chat tool-category selection, localStorage-backed).\n- `src/layouts/` - `MainLayout.tsx` and `AddComp.tsx`.\n- `src/lib/` - `auth-client.ts` (Better Auth React client), `utils.ts` (`cn`), scanner engines, sleep helpers.\n- `src/services/` - pure calculation helpers (BMR, body composition, nutrient calculation), not HTTP clients.\n- `src/utils/` - logging, user preferences, date helpers, misc. `browserVoiceInput.ts` owns browser speech/recording lifecycle; `hooks/AI/useVoiceInput.ts` appends transcripts to drafts without sending.\n- `src/tests/` - Jest suites mirroring `components`/`contexts`/`hooks`/`services`/`utils`, plus `test-utils.tsx`.\n- `public/locales/<lng>/translation.json` - i18next resources, loaded over HTTP at runtime.\n\nWhen searching, ignore `node_modules/`, `dist/`, and every locale except `public/locales/en/`.\n\n## Translations (i18n)\n\n- Only ever edit `public/locales/en/translation.json`. The other 35 locales are machine-synced through the `sync-translations.yml` workflow and a separate SparkyFitnessTranslations repo; hand-editing them creates conflicts with that pipeline.\n- UI strings go through `useTranslation()` / `t('...')` keys, not hardcoded literals.\n- `en/translation.json` is ~120 KB - grep for the key or section you need instead of reading the whole file.\n- Developer docs: `../docs/src/developer/translations.md`.\n\n## Conventions\n\n- Use `apiCall` (or an existing per-domain client) for backend requests; don't hand-roll `fetch`.\n- Prefer declaring toast text via React Query `meta` on the query/mutation instead of imperative `toast(...)` calls where the global handlers cover it.\n- Use `src/utils/logging.ts` helpers instead of bare `console.*`; verbosity follows the user's logging-level preference.\n- Keep `YYYY-MM-DD` values as calendar-day strings; use the shared timezone/day helpers from `@workspace/shared` instead of `toISOString().split('T')[0]`.\n- To learn a database table's shape, read `../shared/src/schemas/database/<Table>.zod.ts` - do not read `../db_schema_backup.sql` or the migrations.\n- Auth flows go through `src/lib/auth-client.ts` and `useAuth`; acting-user (family access) state lives in `ActiveUserContext` and affects most data hooks.\n- New UI should reuse `src/components/ui/` primitives and existing shared components before adding new ones.\n- Food and meal quantity fields use `NumericInput` with `allowExpressions` for basic arithmetic. `utils/evaluateArithmetic.ts` parses numbers, parentheses, and `+ - * /` without executing code; invalid expressions block form submission. Other numeric fields retain their existing behavior.\n- Time entry uses `TimeInput` (`'HH:mm'`) and `DateTimeInput` (`'YYYY-MM-DDTHH:mm'`) from `src/components/ui/TimeInput.tsx`, never native `<input type=\"time\">` or `type=\"datetime-local\"`: browsers pick 12h/24h from their language and ignore the user's `timeFormat` preference. Both take the native string formats, so call sites keep their state as-is; name them with `aria-labelledby` (an external `<Label htmlFor>` cannot target a multi-part field).\n- **Cache Invalidation on Library Mutations:** When mutating foods, exercises, presets, or plans, use the domain invalidation hooks from `src/hooks/useInvalidateKeys.ts` (`useExerciseInvalidation`, `useFoodInvalidation`, `useMealInvalidation`, `useDiaryInvalidation`) to invalidate the entire family of dependent query keys including search, presets, templates, and diary daily progress.\n- **Library Deletes & Snapshots:** Deleting foods or exercises uses `mode: 'delete'` which preserves logged diary history (via snapshots) and drops items from presets/plans; only explicit `delete_with_history` deletes diary entries. Empty presets are guarded against starting/logging.\n\n## Testing and Validation\n\n- Test files live in `src/tests/`, mirroring the source area they cover; use `test-utils.tsx` for rendering with providers.\n- Run the tests nearest the touched surface first, then `pnpm run validate` for cross-cutting changes.\n- Lint is strict (`--max-warnings 0`); unused imports fail the build.\n\n## Quick Routing\n\n- Routing/navigation/permission issue: `src/App.tsx` (router, `PrivateRoute`, `PermissionRoute`) and `src/layouts/MainLayout.tsx`.\n- API/error-toast issue: `src/api/api.ts`, then the domain client in `src/api/<Domain>/`, then the query/mutation `meta` in the calling hook.\n- Auth/session issue: `src/lib/auth-client.ts`, `src/hooks/useAuth.tsx`, `src/pages/Auth/`, and the server's `auth.ts` if it crosses packages.\n- Family-access/acting-user issue: `src/contexts/ActiveUserContext.tsx` and the hooks consuming it.\n- Chat (Sparky) issue: `src/pages/Chat/`, `src/components/ai/`, `src/api/Chatbot/`. Voice controls live in `VoiceInputControl.tsx`; `VoiceInputSettings.tsx` shares server-backed enable/System/AI choices with mobile. `active_voice_ai_service_id: null` means System, never automatic inheritance from Chat. A compatible Chat model is only a suggestion. Cancelling, leaving the page, hiding the tab, or changing voice preferences releases the microphone and aborts uploads.\n- Theme/preferences issue: `src/contexts/ThemeContext.tsx`, `src/contexts/PreferencesContext.tsx`, `src/api/Settings/preferences.ts`, `src/utils/userPreferences.ts`.\n- Calorie goal settings: `src/pages/Settings/CalculationSettings.tsx`; `components/WeeklyWeightGoalInput.tsx` edits the `weekly` goal mode with direction and kg, g, lbs or stones + pounds per week, defaulting to the profile weight unit. Store signed kilograms in `goal_mode_weekly_change_kg` (negative loss, positive gain, zero maintenance), preserving legacy percentage modes. Use shared `formatWeeklyGoalRate` / `parseWeeklyGoalRate` for display and input; switching units preserves the canonical rate. Pass `weeklyChangeKg` to `computeCalorieTarget`; Diary preview and `CalorieTargetBreakdown` use the same preference.\n- Missing/wrong UI text: the i18n key in `public/locales/en/translation.json` and the `t('...')` call site.\n- Exercise alternatives / workout feedback / adaptive suggestions (#1560): `AddExerciseDialog`'s `replaceFor` prop adds the **Suggested** tab (`pages/Exercises/ExerciseAlternativesPanel.tsx`); the workout player's Replace (lazy-loaded dialog) and load pass (`WorkoutPlaybackPage.tsx`, helpers in `utils/workoutPlayback.ts`) apply the shared `decideAdaptiveAdjustment` rules and keep both the usual and adapted sets on the draft for \"Use my usual\"; feedback is `pages/Diary/WorkoutFeedbackPanel.tsx` (finish dialog + expanded diary workout) via `hooks/Exercises/useWorkoutCoaching.ts`.\n- Medication adherence, streak, checklist progress and month-calendar status (`pages/Medications/TodayMedications.tsx`, `MedicationLogCalendar.tsx`) are computed by `@workspace/shared` (`medications/adherence.ts`), the same code the mobile adherence screen uses; change the rules there, not in the page. The meds/supplements filter rules (`filterMedsBySubtype`, `filterEntriesBySubtype`, `visibleDoseCards`) come from `medications/subtype.ts`.\n- Symptom or episode tracking (#1882): `src/pages/Symptoms/` (`SymptomsHub` is the tracker, shown on Check-in through `SymptomsPanel` and on Medications with `variant=\"medications\"`; `SymptomLogForm` is template-driven, `SymptomsReport` is Reports > Symptoms), `src/api/Symptoms/symptomService.ts`, `src/hooks/useSymptoms.ts`. Which sections a symptom asks about comes from `resolveSections` in `@workspace/shared`; family delegates with only the symptoms permission use the standalone `/symptoms` page.\n- Bodyweight exercises (#56): the `bodyweight_reps` layout in `constants/exercises.ts` (`signedWeight`) drives the +/− weight header and placeholder in the set editor; reports and trend charts count body weight through the shared `effectiveLoadKg` / `setVolumeKg` (`pages/Reports/ReportsTables.tsx`, `utils/exerciseTrendUtils.ts`, which reads `body_weight_kg` from the progress endpoint).\n- `weight_distance` / `weight_duration` (#127) have their own `SET_TABLE_LAYOUT` entries: carries swap the reps cell for a distance cell (stored km, edited in metres/yards via `carryDistanceFromKm` / `carryDistanceToKm` in `@workspace/shared`), loaded holds drop reps and use the duration column.\n- Training consistency (#59): `pages/Reports/TrainingConsistencyCard.tsx` in the Exercise report's left column, fed by `useTrainingConsistency` (`hooks/Reports/useReports.ts`) from `GET /reports/training-consistency`. A fixed 26-week window computed on the server by the shared `buildTrainingConsistency`, so it ignores the report's date filter; the existing `WorkoutHeatmap` stays the calendar.\n- Compare tab (day and week questions): `src/pages/Reports/Compare/CompareReport.tsx` holds the question and span state and wires the sentence, charts and tables; wording is `compareCopy.ts` and table rows are `compareTables.ts`; question, window and display-scale logic is `shared/src/compare/`. Since-habit questions are not offered on the web yet. It shows only the signed-in user's own data (no request while acting on behalf), and its range is its own, so the Reports date picker is hidden on this tab.\n- Shoe inventory: the Equipment section of the Exercises page (`src/pages/Exercises/EquipmentManager.tsx` + `ShoeFormDialog`; there is no separate tab or route), `src/components/Shoes/ShoeSelect.tsx` (the picker used by `pages/Diary/EditExerciseEntryDialog.tsx` for cardio entries), `src/api/Shoes/shoeService.ts`, `src/hooks/Shoes/useShoes.ts`. Distances are stored in km and edited in the user's unit; assigning a shoe invalidates both `shoeKeys` and `exerciseEntryKeys` because mileage is derived from linked entries. The Equipment section also shows while acting on behalf of someone: `can_manage_diary` delegates can edit it and `can_view_reports` delegates read it, as the server enforces.\n- Training programs (run and walk plans): the `/exercises/training-programs` route (`src/pages/Exercises/TrainingPrograms/`: `TrainingPrograms` is the page, `TrainingProgramPicker` the built-in list with the 3 or 4 runs a week choice and the person's own plans, `StartProgramDialog` the start day, `TrainingProgramCard` the running program, `TrainingProgramCalendar` the month view, projected finish day, goal-date fit and catch-up), reached from `TrainingProgramsEntry` on the Exercises page, and `pages/Diary/RunProgramBanner.tsx` in the Diary's exercise card. Data is `GET/PUT /api/v2/run-program` plus `POST .../complete` and `GET .../custom` through `src/api/Exercises/runPrograms.ts` and `src/hooks/Exercises/useRunProgram.ts`; the server route is owner-only (`requireSelfActor`), so every hook and the banner stay off while `isActingOnBehalf`. All schedule maths comes from `@workspace/shared` (`projectRuns`, `projectedFinishDay`, `programRunForDate`, `catchUpFor`, `buildProgramTimeline`), never re-derived here. Request bodies come from `utils/runPrograms.ts`; `useSaveRunProgramMutation` runs changes one at a time (mutation `scope`) and accepts a function of the program as it stands, so two quick weekday taps both land. Mutations write the response into `runProgramKeys.current()` and call `useRunProgramInvalidation` (program, workout plan, exercise entry and daily progress families); `useDiaryInvalidation` and `useExerciseInvalidation` also refresh `runProgramKeys.all`. English program names, descriptions and week layouts live in `constants/runProgramCopy.ts` and mirror `trainingPrograms.programs.<id>.*` in the locale file. The web does not record runs: **Mark workout as done** (card and Diary) is how a workout is ticked off. Not on web yet: creating or editing custom plans, the race plan builder, the effort rating, the ease-back offers and the workout detail screen.\n- Heart rate zones: Settings → Profile & Account → Heart Rate Zones (`src/pages/Settings/HeartRateZoneSettings.tsx`, `src/hooks/Settings/useHeartRateZones.ts`, `src/api/Settings/heartRateZones.ts` over `GET`/`PUT /api/v2/heart-rate-zones`). Zone maths is shared (`@workspace/shared` `heartRateZones.ts`); labels are in `src/utils/heartRateZoneLabels.ts`. `ActivityHeartRateZonesChart` takes `zoneMethod` (from the stored `zone_method` of the workout's zone rows) and says which method the zones were worked out with; stored zones are never recomputed when settings change.\n- Routing provider (Valhalla, route generation and Follow roads): a Settings > External Providers type like Mealie, with `pages/Settings/RoutingProviderFields.tsx` (empty server URL with an example placeholder and no default server, optional API key, privacy and public-server notes) used by `ProviderSpecificFields` (add) and `EditProviderForm`; the type row is seeded by the server migration, the server resolves it per person (see the server guide).\n- Chart issue: Recharts usage in the domain page plus `src/components/ExerciseCharts/` or `ZoomableChart.tsx`.\n\n## Priority Rule\n\n- For work inside `SparkyFitnessFrontend/`, this file wins over repo-root guidance on package-specific details.\n- If a task spans packages, combine this guide with the other affected package guides.\n- If you add a new domain folder, route family, or cross-cutting convention, update the Domain list, Source Map, and Quick Routing sections of this file in the same change.\n","SparkyFitnessMobile/AGENTS.md":"# AGENTS.md\n\n_Last updated: 2026-10-23_\n\nSparkyFitness Mobile is a React Native 0.86 + Expo SDK 57 app for syncing Apple Health / Health Connect data with the SparkyFitness backend, tracking nutrition, hydration, fasting, measurements, exercise, saved foods, meal templates, custom exercises, workout presets, iOS / Android widgets, the active workout HUD, and the Sparky AI chat.\n\nThis is the package guide for `SparkyFitnessMobile/`. Work from this directory for mobile implementation and validation. If a task crosses into the backend, frontend, or `shared/`, read that package guide too before editing outside mobile.\n\n## Scope And Style\n\n- TypeScript is strict. Keep changes type-safe and compiling cleanly.\n- Prefer small, direct changes that fit the existing screen, hook, and service boundaries.\n- For ambiguous bugs, prove which layer is failing before patching. One narrow diagnostic check beats speculative edits across multiple layers.\n- Do not replace a working implementation with a rewrite unless the requester explicitly approves that direction.\n- When asked to plan work, confirm scope with clarifying questions before exploring code or drafting the plan.\n- Run scripts from `SparkyFitnessMobile/`, except root package operations such as `pnpm install` for patched dependencies.\n- Treat `android/` and `ios/` as generated output when possible. Edit `app.config.ts`, `plugins/`, `targets/`, JS/TS sources, or patch files first, then regenerate with prebuild when needed.\n\n## Stack And Imports\n\n- Primary stack: React 19.2, React Native 0.86, Expo SDK 57, TypeScript 6, React Navigation 7, TanStack Query 5, Uniwind / TailwindCSS v4, Reanimated 4, Skia, Victory Native, Expo Background Task / Task Manager / Notifications, Zustand, assistant-ui + AI SDK (chat).\n- `@/*` maps to this package and `@workspace/shared` maps to `../shared/src/index.ts`.\n- Prefer `@workspace/shared` schemas, constants, date/timezone helpers, and types over local duplicates.\n- The app talks to the backend under `/api`; health uploads go to `POST /api/health-data`.\n- Global `fetch` is Expo's WinterCG `expo/fetch`, so React Native's `{uri, name, type}` FormData file parts throw \"Unsupported FormDataPart implementation\". Append an `expo-file-system` `File` (it implements Blob) for multipart uploads; see `pregnancyPhotosApi.ts`.\n- Server-stored distance/weight units are metric. UI conversion belongs in mobile helpers such as `unitConversions.ts`.\n\n## Localization contract\n\nEnglish (`en`) is the canonical source locale and the deterministic fallback. Feature developers must add/update the English catalog, use semantic static keys, provide explicit English fallback/defaultValue text, use count-based i18next pluralization, use app-locale date/number/unit formatters, and avoid user-facing hardcoded text. Custom, user, and server content stays literal.\n\nFeature developers do not need to know or translate Polish or any future language, and do not need to wait for Weblate. Translators/Weblate own Polish and future translations and linguistic QA. Missing translation content is non-blocking and falls back to English; existing translated content remains structurally validated.\n\nThe shipped locale registry is `src/localization/localeRegistry.json`, read through the typed accessors in `src/localization/localeRegistry.ts`. Adding a catalog to Weblate does not ship it. Shipping requires explicit registry enablement plus native/platform support verification. RN catalogs and native resources are separate surfaces (Expo metadata, Android widget resources, and iOS widget/Live Activity `.lproj` resources), each its own Weblate component and each synced by `.github/workflows/sync-translations.yml`. Only the `en` side of any surface is edited by hand; the i18n audit blocks on registered locales and reports unregistered ones without failing. The workflow pulls the widget resources for registered locales only, because Android and iOS compile whatever resource directories are present and there is no build-time registry check for them.\n\n## Commands\n\n```bash\npnpm start\npnpm run ios\npnpm run android\npnpm run lint\npnpm run typecheck\npnpm run validate\npnpm exec jest --watchman=false --runInBand\npnpm exec jest --watchman=false --runInBand <test-path>\npnpm run test:coverage -- --watchman=false --runInBand\nnpx expo prebuild --clean\n```\n\n- `pnpm run validate` runs i18n and muscle-figure generation checks, typecheck, lint, i18n audit, watch i18n audit, Knip (`pnpm run knip` for unused files and exports), native locales check, and Prettier check together.\n- Use Watchman-disabled Jest commands in agent/sandbox runs; bare Jest often fails on macOS.\n- `collectCoverage` is enabled in Jest config, so expect coverage output from normal test runs.\n- Run `npx expo prebuild --clean` after native dependency changes, permissions, app group or widget target changes, Expo plugin changes, native config edits, or patching native modules.\n- After editing the root `patches/react-native-health-connect@4.1.3.patch`, run `pnpm install` from the repo root, then prebuild from mobile.\n\n## App Shell And Navigation\n\n- `App.tsx` is the root composition point. `App()` wraps `QueryClientProvider`, `KeyboardProvider`, `GestureHandlerRootView`, and `BottomSheetModalProvider`; `AppContent()` owns `NavigationContainer`, `SafeAreaProvider`, navigators, `AddSheet`, auth modals, the embedded/floating active-workout bars, the tab-bar `WhatsNewBanner`, and toasts.\n- App-shell logic lives in dedicated hooks that `AppContent` composes: `useAppBootstrap` (`src/hooks/useAppBootstrap.ts`) owns language initialization, initial-route selection, the initial linking-enabled state, and splash hiding; `useAppStartup` (`src/hooks/useAppStartup.ts`) owns the one-time startup services (theme, notifications, notification actions, background sync, HealthKit observers); `useAutoSyncOnOpen` (`src/hooks/useAutoSyncOnOpen.ts`) for cold-start/foreground-return sync and the observer-yield window; `useAddSheetActions` (`src/hooks/useAddSheetActions.ts`) for AddSheet handlers and last-active-tab tracking. Error-boundary-wrapped `Safe*` screen components live in `src/navigation/safeScreens.tsx`.\n- Startup initializes theme, haptics, sounds, notification prefs, logs, timezone bootstrap, background sync, pending cache refreshes, fasting/hydration card visibility, and platform health observers.\n- Initial route comes from `getActiveServerConfig()` inside `useAppBootstrap`: no active config lands on `Onboarding`; otherwise users enter `Tabs`. A language-initialization failure is logged and never changes the route.\n- Deep links are enabled only after startup confirms `Tabs`, so widget links do not bypass first-run onboarding.\n- `CompareReport` is a stack route (`SafeCompareReport`, title `screens.compareReport`) opened from the Reports hub.\n- Navigation source of truth is `App.tsx` plus `src/types/navigation.ts`; update both and the linking config when routes change.\n- Root stack uses `@react-navigation/native-stack`. Tabs use `@react-navigation/bottom-tabs`.\n- Tabs are `Dashboard`, `Diary`, `Add`, `Library`, and `Settings`. `Add` is a center action in `CustomTabBar`, not a content screen.\n- Native iOS Liquid Glass tabs use `@bottom-tabs/react-navigation` in `src/components/TabsLayout.tsx`; each content tab is wrapped in its own `createNativeStackNavigator` so the tab path still gets native headers. The `Add` tab uses `role: 'search'`, which the iOS 26 SDK draws as the detached round button. The iOS 27 SDK no longer does, so `patches/react-native-bottom-tabs@1.4.0.patch` (a pnpm patch) leaves the role off on iOS 27+ and sets the tab controller's `prominentTabIdentifier` (the iOS 27 API for a separated trailing tab) to that tab instead. Drop the patch once the library does this itself.\n- `TabsLayout` switches at runtime via `useNativeIOSTabsActive()` (`services/nativeTabBarPreference.ts`): native tabs require iOS 26+ Liquid Glass support AND the user's `liquidGlassTabBarEnabled` preference (opt-in). Everything else renders `CustomTabBar` (`TAB_BAR_HEIGHT = 56`); `ActiveWorkoutBar` reads the native tab-bar height instead when native tabs are active.\n- When adding a root-stack screen, add the route to `RootStackParamList` and register a matching `<Stack.Screen>` in `App.tsx` with `createStackScreenOptions(...)` or equivalent explicit iOS native-stack header options.\n- Native header option/button builders live in `utils/nativeHeaderItems.ts` (`createIOSNativeHeaderOptions`, `createIOSSmallNativeHeaderOptions`, text/icon button items); header action colors come from `useHeaderActionColors`, which is Liquid-Glass-aware on iOS.\n- Root-stack screens with a screen-owned React header are automatically checked by `__tests__/navigation/nativeHeaderContract.test.ts`; do not add screen-specific native-header allowlists.\n- Root-stack screens with a screen-owned React header and a real back button must set `headerBackTitle` or `headerBackButtonDisplayMode: 'minimal'` in `App.tsx` so iOS back labels stay explicit or intentionally hidden; close/cancel modal headers do not need either option.\n- Declare screen headers with `useScreenHeader(config)` (`src/hooks/useScreenHeader.tsx`). One declarative descriptor (`title`/`nativeTitle`, `left`/`right` items of kind `back`/`dismiss`/`text`/`icon`/`primary`/`menu`, `busy`/`disabled`, `animateKey` for view↔edit cross-fades) renders both paths: on the native path it mirrors items into `unstable_header{Left,Right}Items` via a layout effect and returns `null`; on the custom path it returns the bar element for the screen to render. A `menu` item is a declarative dropdown (plain actions and/or titled single-select sections, optional accent-dot `showsBadge`): the native path builds a system UIMenu header button, the custom path renders the trigger plus an `AnchoredMenu` under it — one item list drives both, as in `FoodsLibraryScreen`'s ownership filter. Hook screens must not keep hand-rolled header code (no `unstable_header*Items` blocks or custom bars alongside the hook) — the contract test enforces this.\n- Path selection is `useNativeIOSHeadersActive()` (`services/nativeTabBarPreference.ts`): always false on Android; on iOS it is true below iOS 26 (classic native headers) and follows the Liquid Glass toggle on iOS 26+, so turning the toggle off swaps in the same screen-owned fallback headers Android renders.\n- One-accent rule: exactly one primary header action per screen (`kind: 'primary'` or `role: 'primary'`), enforced with a `__DEV__` throw; primary header save buttons fall back to the localized `t('common.save')` / `t('common.saving')` labels when no `label`/`busyLabel` is supplied. Footer-save forms mark their header Save `placement: 'native-only'` so the custom bar does not duplicate the sticky-footer button. `onPress` handlers dispatch through the hook's internal ref map — do not add per-screen handler-ref effects for native header buttons.\n- A **right-slot** `kind: 'primary'` press is wrapped in the shared synchronous duplicate-press guard (`utils/duplicatePress.ts`, same one `FooterSaveBar` uses): presses inside `DUPLICATE_PRESS_WINDOW_MS` collapse to one. `disabled`/`busy` are React state and have not committed when taps queued behind a blocked JS thread replay, so every queued press otherwise ran the handler again (#2191). The guard is deliberately time-based, not a latch on `busy`, because handlers that never report a pending state would leave the button dead. **Left-slot** primaries are exempt — that slot is navigation, and `CycleOnboardingScreen` uses the sugar for a wizard Back where repeated presses are intended. A screen that needs a rapidly repeatable right-slot action must not use `kind: 'primary'`.\n- If a root-stack screen is intentionally presented above `Tabs` instead of inside native-tabs mode, document it in `NATIVE_TABS_ROUTE_EXCLUSIONS` in `__tests__/navigation/nativeHeaderContract.test.ts` with a short reason.\n- Screens intentionally off the hook (e.g. `FoodSearchScreen`'s bespoke anchored-menu bar) must mirror custom actions with `unstable_header{Left,Right}Items` themselves, hide the screen-owned React header behind `useNativeIOSHeadersActive()` with a guard such as `{!usesNativeHeader && <Header />}`, and gate the `useLayoutEffect` that sets native header items on the same flag; otherwise iOS renders both headers.\n- When adding a tab, update `TabParamList`, `NativeTab.Screen`, and `FallbackTab.Screen`; for content tabs also add a tab-local native stack screen using `createIOSNativeHeaderOptions(...)`.\n- `__tests__/navigation/nativeHeaderContract.test.ts` enforces this native-header wiring. If it fails, fix the route/type/navigator alignment instead of weakening the test.\n- Current stack screens include onboarding/tabs, library/detail/form flows for foods/meals/meal plans/exercises/presets, food entry view/edit, meal type detail and copy, the family diary flows (`FamilyMembers`, `FamilyDiary`, `FamilyMealDetail`, and `FamilyCopyReview`), `EditBarcode`, food search/entry/scan/photo flow, workout/activity add/detail, exercise/preset search, settings subscreens, logs, sync, measurements, the progress photo flow (`ProgressPhotos`, `ProgressPhotoCompare`, and `ProgressPhotoTimelapse`), `ExerciseStatistics` and `CardioSession` (with `ActivityShare`, opened from its Share as image button: a 4:5 `ActivityShareCard` of stats and the GPS route over a picked photo or a transparent background, captured with Skia `makeImageFromView` in `services/activityShareImage.ts` and shared as a PNG; color from `utils/shareColor.ts`), `Shoes` and `ShoeForm`, fasting, mindfulness (`MindfulnessDetail` and `MindfulnessLog`), and `WhatsNew`.\n- `AddSheet` offers Food, Workout, Activity, Preset, Measurements, Scan Food, Progress Photos, Mood, Mindfulness, Ask Sparky, and Sync Health Data. Its Mood row opens `MoodLog`, and the Dashboard has a toggleable `mood` card (today's mood, 7-day trend) that opens it (mood faces, tags, note, and a mood-over-time chart; one entry per day via `POST /api/mood`). Its Progress Photos row opens `ProgressPhotos` on the diary's active date. Its Mindfulness row opens `MindfulnessLog` (`MindfulnessLogScreen`), a root-stack modal laid out like `MoodLog` and `MeasurementsAdd` (screen header, scrolling form, sticky `FooterSaveBar`, so the navigator owns Android's Back); `MindfulnessDetail` opens the same screen from the Log Mindful Session row on its Practice & Log tab and, with a `session` param, from Edit on its History tab, so it has no log form of its own. Session type names and icons come from `utils/mindfulnessSessionTypes.ts`, shared by the log screen, the detail screen and `MindfulnessCard`; the list is the shared `mindfulnessSessionTypeSchema`, and a stored type it does not have shows as Other and is kept on save. Paced Breathing and the Meditation Timer keep Finish & Save on screen while the save runs, so they go through `useSaveOncePerRun`: a run is saved once (presses are dropped until the practice is reset, taps replayed before that reset renders included), a failed save can be retried, and a save from a run closed while pending does not reset the next one. `MindfulnessLog` goes back after a save only while it is still focused, because it can be opened from a screen other than the tab host. Keep `AddSheet`'s present/dismiss refs intact to avoid Android re-present loops. Android's hardware Back in `AddSheet` does what the sheet's own Back link does: from the Exercise menu it returns to the main menu, from the main menu it closes the sheet.\n- Current stack screens include onboarding/tabs, library/detail/form flows for foods/meals/meal plans/exercises/presets, food entry view/edit, meal type detail and copy, the family diary flows (`FamilyMembers`, `FamilyDiary`, `FamilyMealDetail`, and `FamilyCopyReview`), `EditBarcode`, food search/entry/scan/photo flow, workout/activity add/detail, exercise/preset search, settings subscreens, logs, sync, measurements, the progress photo flow (`ProgressPhotos`, `ProgressPhotoCompare`, and `ProgressPhotoTimelapse`), `ExerciseStatistics` and `CardioSession`, `Shoes` and `ShoeForm`, fasting, mindfulness (`MindfulnessDetail` and `MindfulnessLog`), and `WhatsNew`.\n- `AddSheet` offers Food, Workout, Activity, Preset, Measurements, Scan Food, Progress Photos, Mood, Mindfulness, Ask Sparky, and Sync Health Data. Its Mood row opens `MoodLog`, and the Dashboard has a toggleable `mood` card (today's mood, 7-day trend) that opens it (mood faces, tags, a Journal box, and a mood-over-time chart). `useMoodCheckinsSupport` probes `GET /api/mood/date/<today>` with `X-Mood-Model-Version: 2` the first time the `mood` card, `MoodLog` or `MoodDay` mounts, and the answer is cached for the session: an array back means the server takes several check-ins a day and any other answer keeps the one-entry-per-day screen via `POST /api/mood`. A failed probe with nothing known shows a Retry on `MoodLog` and `MoodDay` (`components/mood/MoodSupportCheckFailed`) rather than either form, because the one-entry POST on a v2 server edits the day's latest check-in instead of adding one. On a v2 server the `mood` card shows one mood per day (the average of your own moods, else the Garmin or imported one), its body always opens `MoodDay` (the day's check-ins in time order with the day's averages; tap to edit in `MoodLog`, swipe or long-press to delete, Add check-in for a new one), which replaces itself with `MoodLog` for the day on an older server, and its \"+ Log\" (shown when no mood is logged) opens `MoodLog` for a new check-in. `MoodLog` takes an optional `checkinId` to edit; a new check-in for today goes on the profile zone's today even when the dashboard passed the device's day (`utils/moodDay.ts` `moodDayInZone`, which `MoodDay` also uses so its list shows that day; its redirect to the one-entry form keeps the requested day); each face writes the shared `moodFaceValue` (Sad 10 to Excited 90, as on the web); every field is optional but at least one is required, the mood can be left unset (tapping the selected face clears it), the time row opens `TimeSheet` and stays on the current time until set, and edit mode has Delete check-in. The mood report and history chart use one value per day. It also has optional 1-5 Energy and Focus taps (`components/mood/RatingSelector`) that are sent only when changed on the screen (absent means keep, `null` means clear); `useMoodRatingsSupport` applies only to the one-entry-per-day form; it hides them when the server's mood rows lack `energy_value` and shows them when there are no rows to tell, and a save whose response lacks the key shows a one-time \"update your server\" toast. Its Progress Photos row opens `ProgressPhotos` on the diary's active date. Its Mindfulness row opens `MindfulnessLog` (`MindfulnessLogScreen`), a root-stack modal laid out like `MoodLog` and `MeasurementsAdd` (screen header, scrolling form, sticky `FooterSaveBar`, so the navigator owns Android's Back); `MindfulnessDetail` opens the same screen from the Log Mindful Session row on its Practice & Log tab and, with a `session` param, from Edit on its History tab, so it has no log form of its own. Session type names and icons come from `utils/mindfulnessSessionTypes.ts`, shared by the log screen, the detail screen and `MindfulnessCard`; the list is the shared `mindfulnessSessionTypeSchema`, and a stored type it does not have shows as Other and is kept on save. Paced Breathing and the Meditation Timer keep Finish & Save on screen while the save runs, so they go through `useSaveOncePerRun`: a run is saved once (presses are dropped until the practice is reset, taps replayed before that reset renders included), a failed save can be retried, and a save from a run closed while pending does not reset the next one. `MindfulnessLog` goes back after a save only while it is still focused, because it can be opened from a screen other than the tab host. Keep `AddSheet`'s present/dismiss refs intact to avoid Android re-present loops. Android's hardware Back in `AddSheet` does what the sheet's own Back link does: from the Exercise menu it returns to the main menu, from the main menu it closes the sheet.\n- `useNavigationActionGuard` locks navigation-triggering actions while a native-stack transition is running (idle-callback unlock on re-focus, 5s safety release) so double-taps cannot queue duplicate screens; Library create actions use it.\n- `ActiveWorkoutBar` is mounted outside normal screen trees, uses the root navigation ref, and hides itself on modal/editor routes such as food search/forms/scan/photo, exercise search, workout/activity add, measurements, mindfulness log, and barcode edit.\n- Most screens are wrapped with `withErrorBoundary(...)`; `SettingsScreen` also uses section-level recovery so settings remain reachable.\n\n## Source Map\n\n- `src/components/` - reusable UI, charts, settings rows, custom tab bar, add sheet, workout HUD, form chrome, library rows, diary rows, serving sheets, food/workout editors, fasting UI, writeback UI, and `ui/` primitives.\n- `src/components/auth/` - MFA UI shared by onboarding, setup, and reauth.\n- `src/components/compare/` - the Compare report's sentence, span control, start state, answer/checks cards and dots/bars/over-time charts, plus the since-a-habit-started pieces (`CompareSinceRows`, `CompareSinceCard`, `CompareSinceChart`); copy comes from `utils/compareCopy.ts` / `utils/compareSinceCopy.ts`, the since line steps from `utils/compareSinceLine.ts`, metric labels from `constants/compareMetrics.ts`; the question model (spans, windows, suggested questions, recents helpers) and chart data (paired dots, trend line, over-time and since chart data) come from `@workspace/shared` (`shared/src/compare/question.ts`, `shared/src/compare/chartData.ts`).\n- `src/components/glp1/` - GLP-1 tracker cards (injection log, site body map, modeled level, pen/vial inventory, titration, daily check-in) used by `Glp1TrackerScreen`; hooks in `useGlp1.ts` (query keys sit under the `medications` root so `invalidateMedicationEntryCaches` refreshes them). Body-map geometry and the contract types come from `@workspace/shared`.\n- `src/components/medications/` - dose rows, the medication list row and the adherence section: `AdherenceOverviewCard` (stat tiles, ring, 14-day bars, streak), `MedicationChecklistCard`, `MedicationDoseListCard` (due and as-needed rows over `DoseRow`), `MedicationLogCalendar` (month grid, medication filter, legend, symptom dots; owns its own entries and symptom queries), `MedicationDayLog` (the selected day's entries with edit and delete) and `useMedicationStatusColors` (theme tokens for all of them). The screens are `MedicationAdherenceScreen` (route `MedicationAdherence`, opened from the \"Adherence & log\" row on `MedicationsListScreen`) and `MedicationEntryEditScreen` (route `MedicationEntryEdit`, a modal that takes the entry as a param). All adherence, streak, checklist and calendar-status maths comes from `@workspace/shared` (`medications/adherence.ts`, shared with the web Medications page); `hooks/useMedicationAdherence.ts` feeds it one entries window ending on the selected day, and `utils/medicationAdherence.ts` holds the mobile-only helpers (as-needed selection, month shifting, edited-time conversion). The `All | Meds | Supplements` filter helpers also live in shared (`medications/subtype.ts`). Every dose change goes through `invalidateMedicationEntryCaches`, which refreshes the diary card, adherence window, month calendar and daily summary together; injection rows are edited and deleted through the injection endpoints (`useUpdateInjection`, `useDeleteInjection`). The calendar's symptom dots read `useSymptomEntries`, need the separate symptoms permission (without it the dots are simply absent), and never show `source: 'cycle'` symptoms.\n- `src/screens/` - top-level route destinations: dashboard, diary, family member/diary/meal/copy-review flows, settings, sync, logs, Whats New, fasting, food search/scan/photo, library CRUD flows, workout/activity flows, and measurement entry.\n- `src/navigation/` - navigation-level modules such as `safeScreens.tsx`, the error-boundary-wrapped screen components registered in `App.tsx`. (`FoodPhotoFlow` lives in `src/components/`.)\n- `src/hooks/` - TanStack Query hooks, auth/connection hooks, library/search/mutation hooks, measurement/water/check-in hooks, fasting hooks, workout form hooks, widget sync, query client, query keys, and cache helpers.\n- `src/services/api/` - backend clients. `apiClient.ts` handles normal API auth/proxy headers; `healthDataApi.ts`, `aiSettingsApi.ts`, food-photo estimate, and other raw fetch paths must keep auth, proxy, timeout, and session-expiry behavior aligned.\n- `src/services/healthconnect/` - Android Health Connect reads, native aggregation, transformation, enrichment, preferences, and writeback.\n- `src/services/healthkit/` - iOS HealthKit reads, statistics aggregation, transformation, background delivery, preferences, and writeback.\n- `src/services/shared/` - platform-agnostic health helpers: the `collectHealthData` / `runForegroundSync` engine both orchestrators share, the per-run workout-telemetry budget and its reuse cache, Health Connect error classification, sample downsampling, day aggregation/transformation, preference factories, and permission migration/sets.\n- `src/services/` - platform health orchestration, writeback re-exports, background sync, auto-sync coordination, diagnostics, calculations, logging, storage, theme, haptics, sounds, notifications, food photo intro, meal selection, boolean preferences, card visibility, and workout drafts.\n- `src/stores/` - Zustand stores, including the persisted active workout/rest timer store.\n- `src/utils/` - date helpers, unit conversion, food details, meal nutrition, nutrient display, workout/session helpers, fasting formatting, numeric input, concurrency, sync utilities, duplicate-press guarding, photo estimate error mapping, and rate limiting.\n- `src/constants/` - meal, exercise, fasting, and nutrient metadata.\n- JS bridges to native modules live in `src/services/` (`CalorieWidgetBridge.ts`, `ExactAlarmBridge.ts`); there is no `src/native/` directory.\n- `plugins/`, `targets/widget/`, `targets/android-widget/`, `targets/android-exact-alarm/` - Expo plugins and widget/native extension sources.\n\n## React Query And Local State\n\n- Query setup lives in `src/hooks/queryClient.ts`; keys live in `src/hooks/queryKeys.ts`.\n- Default `staleTime` is `Infinity`, so mutations must explicitly invalidate or update affected caches.\n- `refreshHealthSyncCache` invalidates daily summary, measurements, sleep (`sleepDayQueryKey` / `sleepRangeQueryKey`), and exercise history after a health sync. Sleep is in that set because a partial observer upload would otherwise sit in the diary until the app is reloaded.\n- `useRefetchOnFocus(refetch, enabled)` is the standard focus-refresh hook.\n- `useCompare(question, window)` wraps `fetchCompare` on `compareQueryKey` for a day or week question; it is disabled until the question is askable (and for a since question), refetches on focus, and reports `serverTooOld` for a 404. `useCompareSince(question, habitStart, line)` wraps `fetchCompareSince` on `compareSinceQueryKey` (`habitStart` and `line` are null until picked and then left to the server); its `serverTooOld` is a 404 or a 400 whose JSON body names `details.mode` (an older server rejecting `mode=since`), while any other 400, such as a start out of range, is an ordinary error. `compareRecentQuestions` (appPreferencesStore, newest first, max 5) is written by `CompareReportScreen` once a distinct question, since questions included, has an answer.\n- `useFoodsLibrary` is an intentional exception with an infinite query, finite stale window, and `resetQueries(...)` refreshes so focus/pull refresh reloads page 1 instead of every cached page.\n- Meal mutations invalidate meals, recent meals, search, and details; food entry creation can affect recent meals.\n- Exercise/workout preset list/search/detail invalidation belongs in their mutation hooks.\n- `useUpsertCheckIn` updates measurement queries and calls `refreshHealthSyncCache(queryClient)`.\n- `useWaterIntakeMutation` fetches `waterContainersQueryKey`, persists the selected container, and optimistically updates `dailySummaryQueryKey(date)`.\n- Active-server switches clear React Query state before refetching connection state.\n- Error-boundary retry flows call `queryClient.resetQueries()`.\n- App-local toggles live in `stores/appPreferencesStore.ts` (Zustand `persist`, single AsyncStorage key `@SparkyFitness/app-preferences`): haptics, sounds, notifications, hydration/fasting card visibility, Ask Sparky and progress photos card visibility, the Liquid Glass tab bar opt-in, the active-workout metric column, the default rest period (`defaultRestSec`, edited in `WorkoutSettingsScreen`), mood check-in reminders (`checkinRemindersEnabled`, `checkinReminderTimes`, edited in `NotificationSettingsScreen` and offered once after the first check-in), hydration reminders (`waterReminderEnabled`, `waterReminderIntervalHours`, `waterReminderWindowStart` / `waterReminderWindowEnd`, edited in `NotificationSettingsScreen`), the Health Trends layout (`healthTrendOrder` + `hiddenHealthTrends`, edited in `HealthTrendsSettingsScreen`), the Reports layout (`reportOrder`, `hiddenReports`, `reportDefaultRange`, `hiddenReportSections`, `reportNutrientOrder`, `shownReportNutrients`, edited in `ReportsSettingsScreen`), the Compare report's recent questions (`compareRecentQuestions`, newest first, max 5, written by `CompareReportScreen` and read through `knownCompareQuestions`, which drops any a later build no longer knows), and the Apple Watch layout (`watchPageOrder` + `hiddenWatchPages`, the Goals page's `watchNutrientOrder` + `shownWatchNutrients`, and `watchSetInputStyle`, all edited in `WatchSettingsScreen`). The Health Trends pair is written through the single `setHealthTrendLayout(order, hiddenKeys)` setter rather than one per field, because one drag changes order and visibility together; a saved order is reconciled against `constants/healthTrends.ts` by `resolveHealthTrendOrder`, so registering a new graph needs no `STORE_VERSION` bump. Consume via selectors (`useAppPreferencesStore((s) => s.hapticsEnabled)`) plus generated setters; non-React code reads current values through helpers like `getDefaultRestSec()`. A legacy-aware storage adapter migrates the old per-key `@HealthConnect:*` values once. These preferences never sync to the server.\n\n## Health Sync\n\n- `src/services/healthConnectService.ts` is Android orchestration; `src/services/healthConnectService.ios.ts` is iOS orchestration. They are substantial platform implementations, not thin wrappers.\n- Both orchestrators batch metric reads with `runTasksInBatches`, a concurrency of 3, and per-metric timeouts. Preserve timeout and partial-error handling.\n- Bootstrap timezone state before sync. `ensureTimezoneBootstrapped(...)` runs at startup and `healthDataApi.ts` enforces it before upload.\n- Preserve `record_timezone` and `record_utc_offset_minutes` when available.\n- Manual sync, sync-on-open, foreground-return sync, background sync, and iOS observer-triggered sync share coordination logic. Preserve claim/in-flight guards and cooldown recording.\n- Health uploads are chunked. Simple measurements use large chunks; sleep sessions use smaller session chunks; exercise/workout records are grouped by source to match server delete-then-insert behavior.\n- Sync result objects include `syncErrors`; callers should surface partial failures and avoid advancing `lastSyncedTime` when any metric read failed.\n- `backgroundSyncService.ts` uses overlap windows for sessions and day-aligned rolling windows for cumulative metrics and nutrition. Do not collapse those into one naive window.\n- On iOS, cumulative metrics should use HealthKit statistics queries, not raw sample summation.\n- On Android, cumulative metrics (`Steps`, `Distance`, `ActiveCaloriesBurned`, `TotalCaloriesBurned`, `FloorsClimbed`) use Health Connect `aggregateGroupByPeriod` once per range. Native source-priority dedup should match Health Connect UI; do not reintroduce JS `Math.max` or source allowlist dedup.\n- Android read helpers return `{ records, error }` via `readHealthRecordsDetailed` and `aggregateCumulativeMetricByDayDetailed`; legacy wrappers unwrap only records.\n- Android exercise sessions are enriched with `aggregateRecord` for active, total, and basal calories plus distance and steps over the session window. Active/total calories start scoped to `dataOrigin`, while basal energy remains unfiltered; `total - basal` is compared as an active-energy candidate. Incomplete or implausible active/total pairs retry without the origin filter so Health Connect can apply source priority. Distance and steps always stay origin-scoped; never infer workout steps from an unfiltered clock-window query.\n- iOS exercise-session steps must come from statistics attached directly to the `HKWorkout`. Do not infer workout steps by querying all step samples in the workout's clock window.\n- iOS HealthKit locked-device failures surface as database-inaccessible warnings. Do not treat these as successful empty reads.\n- `app.config.ts` grants `android.permission.health.READ_HEALTH_DATA_HISTORY` so Android can read data older than 30 days.\n- Health Connect permission migrations belong in `services/shared/healthPermissionMigration.ts`, not UI-only state. iOS does not use that version: `refreshEnabledMetricPermissions` asks HealthKit through `hasUnrequestedHealthPermissions` and requests only when an enabled type was never asked about, so bump the version for Android permission changes only.\n- Core check-in measurements use `measurementsApi.ts` and `MeasurementsAddScreen`; preserve `upsertCheckIn` omitted-vs-null semantics.\n- \"Import Full History\" (`ImportHistoryScreen`, reached from `SyncScreen`) is a one-time resumable backfill: `backfillService.ts` walks 30-day day-aligned windows newest-first from start-of-today down to a probe-derived floor (`readEarliestRecord` on both providers), one upload per window, with a per-server checkpoint in `backfillCheckpoint.ts`. It never advances `lastSyncedTime` and never runs writeback; while it runs, `isBackfillRunning()` (autoSyncCoordinator) makes background sync skip.\n- The backfill's metric set is FROZEN in its checkpoint at first run; resume uses the frozen set verbatim and toggle changes require Start Over. Quota exhaustion, locked device, and app-inactive are expected mid-run stops — the checkpoint keeps them resumable.\n\n## Health Writeback\n\n- Writeback sends Sparky diary nutrition and hydration back to Apple Health on iOS and Health Connect on Android.\n- Platform split: `services/writeback.ios.ts` re-exports `healthkit/writeback.ts`; `services/writeback.ts` re-exports `healthconnect/writeback.ts`.\n- `runWriteback()` runs after inbound sync in its own try/catch. Writeback failures must not block inbound sync results.\n- Writeback is opt-in per metric and gated on write permissions. Android production permissions include `WRITE_NUTRITION` and `WRITE_HYDRATION`; other write permissions are dev-only.\n- Imported health entries are skipped to avoid echo loops. iOS sets the app bundle id as the own-source guard; Android relies on source metadata.\n- Per-day content-signature hashing skips unchanged days. Each run deletes prior tracked UUIDs then saves fresh records; failed deletes are retried next run.\n- `HealthDataWriteback` on `SyncScreen` owns the remove flow. `BottomSheetPicker` offers all-time purge or date range through `DateRangeSheet`; both call `removeWrittenData(range)` and clear tracking.\n- Inbound iOS nutrition sync reads food correlations with a rolling nutrition lookback and upserts by `(source, source_id)` server-side.\n\n## Native Patches\n\n- `react-native-health-connect` is declared as `^4.1.3`; the installed build is patched from the repo root via `patchedDependencies` in `pnpm-workspace.yaml`.\n- Patch file: `../patches/react-native-health-connect@4.1.3.patch`, one hunk. `PermissionUtils.mapPermissionResult` maps `ExerciseRoute` and `BackgroundAccessPermission` back to JS but not `ReadHealthDataHistory`, even though `parsePermissions` accepts it on the request path. Since `requestPermission` also resolves through `mapPermissionResult`, a granted history permission can never be observed, so `ensureHistoryReadPermission` always returns false and Import Full History believes it is capped at 30 days. Reported upstream as matinzd/react-native-health-connect#276; drop the patch if that lands.\n- The older 3.5.3 patch also forced `getTimeRangeFilterLocal` in `getAggregateGroupByPeriodRequest` (DST / local-day grouping) and made `ExerciseRouteResultType` string-valued. Both are gone: 4.1.3 uses the local filter at all 21 sites, and upstream is fixing the route enum in #274 by typing the read path as a string literal union instead — a better fix than ours, so `routeNeedsConsent` keeps accepting both forms and depends on neither declaration.\n- `@bacons/apple-targets@4.0.6` is patched via `../patches/@bacons__apple-targets@4.0.6.patch`, fixing two upstream bugs. First, its xcode pass matched \"its\" extension target by type with a fall-back to any same-type target, which adopted and corrupted the expo-widgets `ExpoWidgetsTarget` on a clean prebuild; the patch scopes the match to an exact product-name hit. Second, the existing-target update path crashed every non-clean prebuild (EvanBacon/expo-apple-targets#201): removing the old build configuration list's referrers cleared `target.props.buildConfigurationList`, which the next line then dereferenced; the patch holds the list in a local and iterates a copy of its configurations so none are skipped mid-removal.\n- `@bacons/apple-targets` is held at 4.0.6 on purpose: it has a 5.x, and moving to it invalidates the patch file. `expo install --fix` does not touch either patched package (neither is an Expo SDK package), so SDK upgrades leave both patches applying cleanly.\n- `expo-health-connect` was removed. It only ever supplied an Expo config plugin, and upstream archived it: everything it did ships inside `react-native-health-connect` as of v4, which now writes both the `ACTION_SHOW_PERMISSIONS_RATIONALE` intent filter (Android 13 and below) and the `ViewPermissionUsageActivity` alias (Android 14+). Installing both causes Android build failures from duplicate classes.\n- After changing a patch or upgrading a patched package, run `pnpm install` from the repo root and then `npx expo prebuild --clean` from mobile before native validation.\n\n## Food, Meals, Units, And Photo Estimates\n\n- Food search spans local foods, online providers, meals, barcode scan, label scan, and AI photo estimates. Keep `FoodSearchScreen`, `FoodScanScreen`, `FoodEntryAddScreen`, `FoodFormScreen`, `FoodPhotoFlow`, and route params aligned.\n- `LibraryScreen` is the hub for saved Foods, Meals, recurring Meal Plans, Exercises, and Workout Presets. Full lists live in `FoodsLibraryScreen`, `MealsLibraryScreen`, `MealPlansScreen`, `ExercisesLibraryScreen`, and `WorkoutPresetsLibraryScreen`.\n- Food detail/edit flow: `FoodDetailScreen`, `FoodFormScreen`, `FoodForm`, `FoodUnitSelectorSheet`, `useFoodVariants`, `useFoodsLibrary`, `useDeleteFood`, `foodsApi`, and `utils/foodDetails.ts`.\n- `FoodForm` supports equivalent serving sizes grouped by nutrient signature, auto-scale nutrition, compatible unit conversion via `convertServingSizeOnUnitChange`, optional AI cross-category unit conversion, custom nutrients, and caller-provided `headerChildren`.\n- `EditBarcodeScreen` lets users add or remove extra barcodes for a saved food. Keep `FoodDetailScreen`, `EditBarcodeScreen`, `foodsApi`, and the `EditBarcode` route params aligned.\n- Meal templates use `MealAddScreen`, `MealDetailScreen`, `FoodSearch` / `FoodEntryAdd` with `pickerMode: 'meal-builder'`, and `services/mealBuilderSelection.ts` for pending ingredient handoff.\n- Recurring meal plans use `MealPlansScreen`, `MealPlanFormScreen`, `useMealPlans`, `mealPlansApi`, and `utils/mealPlanForm.ts`. Plans can schedule any meal already visible in the user's meal library, including family/public meals, but the plan itself remains private to its owner.\n- Workout plans (the schedule the watch's \"Scheduled today\" and the diary banner read) are created and edited from `Library > Workout plans` with `WorkoutPlansScreen`, `WorkoutPlanFormScreen`, `useWorkoutPlans`, `workoutPlansApi.ts` and `utils/workoutPlanForm.ts`, modeled on the meal plan screens. The form supports weekly and sequential plans and assigns saved workouts (presets); a single-exercise assignment made on the web is preserved on save but only editable there. Switching the schedule type clears the assignments, as the server expects new ones with a type change.\n- Logged-meal grouped diary entries use `foodEntryMealsApi`, `FoodEntryViewScreen`, and `EditLoggedMealScreen`. Preserve stored component nutrition snapshots when editing.\n- `MealTypeDetailScreen` owns single-meal-type day views and copy-to-another-day via `useCopyFoodEntries`; be careful with custom meal types and synthetic buckets.\n- External food providers use provider-agnostic v2 endpoints where possible. Provider categories and barcode support come from server config; do not hardcode provider type allowlists unless preserving an explicit fallback.\n- \"All Providers\" aggregated search (`useAllProvidersSearch`) fans one debounced term out across every active provider in parallel — one `useQueries` entry per provider, results projected in the `combine` callback for structural sharing. Providers stream in independently; a slow or failing provider must not block the others. Open Food Facts calls go through the shared rate limiter.\n- Label scan tries Apple's on-device model first: `modules/on-device-nutrition` (Swift, `FoundationModels` with the label photo as an `Attachment` plus its text from a Vision `VNRecognizeTextRequest` run in the module (greedy sampling), iOS 27+ with Apple Intelligence; compiled out under `compiler(<6.4)`) is wrapped by `services/onDeviceLabelScan.ts`, which validates the extraction (calories, macros, a per-serving size, and any optional nutrient the model filled in must appear as numbers in the returned `ocr_text`, ranges, sugars/fiber <= carbs, energy vs macros) and returns null on any failure or when the device-local `onDeviceLabelScanEnabled` preference (Settings → AI, `AiSettingsScreen`, shown only when the model is available; it also shows the active server provider, read-only) is off, so `FoodScanScreen.handleUsePhoto` falls back to the server `scanNutritionLabel`. The food form filled in from a scan carries `labelScanSource` and shows `LabelScanSourceBanner`: it says which AI read the label and offers the other one (`useRetryLabelScan` reads the kept photo from `services/labelScanSession.ts` again and replaces the form; coming back to the phone needs the setting on and the model available). The Swift side has not been compiled against the iOS 27 SDK from CI-less environments; verify on an iOS 27 device after `npx expo prebuild --clean`.\n- Photo mode is hidden in meal-builder mode because photo estimates log to the diary.\n- `FoodPhotoFlow` is a modal native stack and wraps itself in a local `KeyboardProvider`.\n- Photo availability fetches `GET /api/chat/ai-service-settings/active` through `aiSettingsApi.ts`; food photo is attempt-all, so `isFoodPhotoAvailable` gates only on a configured provider, not a specific provider type.\n- Estimation posts to `POST /api/foods/estimate-food-photo` through `estimateFoodPhoto(...)` in `externalFoodSearchApi.ts` and uses typed `FoodPhotoEstimateError` codes from `@workspace/shared`.\n- Food-photo request/response changes cross package boundaries: update shared schema and server route/service with mobile.\n- Keep `auto_scale_online_imports` separate from Open Food Facts-specific scaling preferences in `FoodSettingsScreen`.\n- Nutrient management lives under Food Settings: `CustomNutrientsScreen` / `CustomNutrientFormScreen` (create, edit, delete; renames and deletes invalidate display, goals and daily-summary caches since the nutrient name is the key), and `NutrientDisplaySettingsScreen` (`mobile` platform only, and only the view groups a mobile screen reads: `food_database`, which `FoodNutrientBreakdown` applies to the food, meal and entry nutrient lists, and `report_tabular`, which `DailyNutritionDetailsScreen` applies; `summary` and `diary` keep their Dashboard/Diary screens, and `quick_info`, `goal` and `report_chart` are not offered until a mobile screen reads them). A nutrient's goal direction is edited on the Goals screen.\n\n## Exercise, Workouts, And Fasting\n\n- Exercise and workout preset flows use `ExerciseSearch`, `PresetSearch`, detail/form screens, paginated/search hooks, mutation hooks, and shared workout payload helpers in `utils/workoutSession.ts`.\n- Session responses are discriminated unions from `@workspace/shared`: preset workouts and individual activity sessions have different shapes. Keep detail/edit screens type-safe.\n- Workout/activity drafts are persisted by `workoutDraftService`; `useWorkoutForm`, `useActivityForm`, and `useDraftPersistence` own form state.\n- Exercise selection returns via `CommonActions.setParams` and a nonce pattern through `useSelectedExercise`.\n- Adaptive coaching (#1560): every Replace action passes `replaceFor` (built by `utils/exerciseReplace.ts`) so `ExerciseSearch` opens on a ranked **Suggested** tab (`components/ExerciseAlternativesList.tsx`, `GET /api/v2/exercises/:id/alternatives`); catalog picks import through `importExercise` first. End-of-workout feedback is `components/WorkoutFeedbackCard.tsx` (on `WorkoutCompleteScreen` and `WorkoutDetailScreen`) backed by `useWorkoutSessionFeedback`, which autosaves serially so the last answer wins. `useLiveCoachingSignals` (mounted in `ActiveWorkoutScreen`) captures signals into `activeWorkoutStore.coachingSignals` (`null` = asked, no history); `resolveLiveAssumedSetValues` applies the shared `decideAdaptiveAdjustment` rules unless the entry is in `declinedAdaptive`, and `AdaptiveSuggestionBanner` always explains the change with a \"Use my usual\" undo. The `adaptive_workout_suggestions` switch in `WorkoutSettingsScreen` is an account setting; `duckMusicDuringCues` (Lower music during cues) is device-local and drives `beginCueDucking`/`endCueDucking` in `services/sounds.ts` — with it off, the audio session is never touched.\n- Bodyweight exercises (#56) use the `bodyweight_reps` modality: a set's weight is signed (+ added, − assistance, blank = body weight alone). Weight inputs parse it with `parseSignedDecimalInput` (`utils/numericInput.ts`; the base `parseDecimalInput` still rejects a sign) only when the modality is `bodyweight_reps`, including the save builders, and switch to a keyboard with a minus key. Volume and the e1RM/10RM columns go through `setVolumeKg` / `setLoadKg` in `utils/workoutSession.ts`, which delegate to the shared `effectiveLoadKg` rule, with body weight from `hooks/useBodyWeightKg.ts` (the latest check-in on or before the day, fetched only when an exercise on screen is bodyweight). Live PRs stay on the added weight, an unweighted set ranking as +0.\n- Weighted carries and loaded holds (#127) use the `weight_distance` / `weight_duration` modalities. Carry distance is typed in metres (yards when the app distance unit is miles) and converted with `carryDistanceFromKm` / `carryDistanceToKm` from `@workspace/shared` (`utils/carryDistance.ts`); always go through `setDistanceFromKm` / `setDistanceToKm` / `modalityStoresDuration` / `modalityStoresDistance` in `utils/workoutSession.ts` rather than branching on modality. Carry distance is excluded from the workout's cardio distance total. The activity edit flow (`EditableSetRow` / `buildActivitySetsPayload`, which takes the distance unit) edits weight plus seconds or distance for these types.\n- Watch support for them: a loaded hold is armed as `timed` + `weighted` (weight box beside the countdown/stopwatch), a carry as `carry` with `targetDistanceKm`; the watch shows and edits the distance in the phone's unit (metres, or yards when the distance unit is miles; the context carries `distanceUnit`, read as `effectiveCarryUnit`) while `SetValues.distanceKm` / `CompletedSet.distanceKm` stay km on the wire, and `useWatchWorkoutBridge` writes it back as `distance`. The distance keypad accepts a tenth. A pending distance edit is dropped if `distanceUnit` changes before that edit commits, and an open distance keypad is dismissed, same as a weight-unit change drops a pending weight edit. `resolveWatchSetTargets` is the single place those targets are built.\n- Rest timer state lives in `stores/activeWorkoutStore.ts`; notifications are scheduled through `services/notifications.ts`. The rest-complete ping carries a background \"Complete Set\" action (`rest-complete` category): responses are routed to `completeActiveSetIfReady` by `initWorkoutNotificationActions` (exported from the store, wired in App startup — the response listener cannot live in `notifications.ts` without a store↔service import cycle), and stale delivered pings are swept when the next rest is scheduled. Exactly one cue fires per rest: the in-app chime (`services/sounds.ts`) while the app is active, otherwise the ping's own sound. Anything that stands down in favour of the chime must test `willPlayRestCompleteSound()` (preference **and** `AppState`), never the preference alone — the notification handler also runs for frontmost-but-inactive states (screen locking, app switcher), and `markRestReady` cancels the due ping only while the app is active so the deadline flip cannot race the OS delivery. The ping ships at the `timeSensitive` interruption level so a Focus mode cannot silence it.\n- Set IDs are preserved server-side across workout edits so the active w","SparkyFitnessServer/AGENTS.md":"# AGENTS.md\n\n_Last updated: 2026-10-22_\n\nSparkyFitness Server is the backend API package for the SparkyFitness monorepo. Use this file as the primary guide for work inside `SparkyFitnessServer/`.\n\n**Quick Links for AI Tools:** See `../agent-docs/README.md` for:\n\n- `file-and-domain-reference.md` — Where to find server code by feature\n- `testing-patterns.md` — How to test routes, services, repositories, and RLS\n- `architecture-permissions.md` — Permission types and RLS patterns\n- `new-migration-checklist.md` — 8-step database change checklist\n\nIf a task also touches `shared/`, the frontend, or the mobile app, read the relevant package guide before editing outside this directory. Use `../AGENTS.md` for monorepo-level context.\n\n## Scope\n\n- This file is for package-local work in `SparkyFitnessServer/`.\n- Keep changes inside this package unless the task clearly crosses package boundaries.\n- This is the single source of truth for the package; `CLAUDE.md` just imports it via `See @AGENTS.md`.\n- Do not invent alternate boot paths, duplicate route registries, or parallel migration flows when the current startup path already covers the behavior.\n\n## Current Snapshot\n\n- Dev boot path: `pnpm start` -> `nodemon` -> `tsx index.ts`\n- `index.ts` loads `../.env`, applies file-backed secrets, runs preflight checks, calls `initializeDatabase()` for migrations and RLS policies, then imports `SparkyFitnessServer.ts`\n- Main app shell: `SparkyFitnessServer.ts`\n- Stack: Express 5, PostgreSQL via `pg`, Better Auth, Zod, TypeScript 6, Vitest 5, ESLint 10\n- Module system: ESM with `type: \"module\"` and `moduleResolution: \"NodeNext\"`\n- The package is now effectively TypeScript-first; almost all source files are `.ts`\n- Main domains: food and meal tracking, exercise logging, health and sleep data, sleep science, fasting, medications, symptom and episode tracking, mood, menstrual cycle and pregnancy, reporting, AI chat, onboarding, identity, admin tooling, and external provider integrations\n\n## Verified Commands\n\n```bash\npnpm start\npnpm run validate\npnpm run typecheck\npnpm run lint\npnpm run lint:fix\npnpm run format:check\npnpm run format\npnpm test\npnpm run test:watch\npnpm run test:coverage\npnpm run test:ci\npnpm exec vitest run tests/mealRoutes.test.ts\npnpm exec eslint routes/v2/foodRoutes.ts services/foodCoreService.ts\n```\n\n- `pnpm start` uses hot reload through `nodemon`; `nodemon.json` ultimately executes `tsx index.ts`\n- `pnpm run validate` runs typecheck, lint, and Prettier check together\n- `pnpm test` runs `vitest run`\n- The backend default port is `3010` unless `SPARKY_FITNESS_SERVER_PORT` overrides it\n- For targeted test runs, prefer `pnpm exec vitest run tests/<name>.test.ts`\n\n## Source Map\n\n- Food assistant state: owner-only preferences, recoverable tasks and operation history use `FoodAssistant.api.zod.ts` / `FoodAssistant.zod.ts` in shared. Server: `routes/v2/foodAssistantRoutes.ts`, `services/foodAssistantService.ts`, `models/foodAssistantRepository.ts`, `ai/tools/foodAssistantTools.ts`. Web: `src/components/ai/FoodAssistantState.tsx`, `src/api/Chatbot/foodAssistantService.ts`.\n\n- `index.ts` - real dev entrypoint; loads env, secrets, and preflight checks before booting the app\n- `SparkyFitnessServer.ts` - Express app shell, route mounting, Swagger/ReDoc, startup, graceful shutdown\n- `services/backgroundJobScheduler.ts` - starts every scheduled background job at startup, including the demo reset; provider syncs are listed in `services/providerSyncScheduler.ts`; `SPARKY_FITNESS_DISABLE_SCHEDULED_JOBS=true` skips them all\n- `auth.ts` - Better Auth configuration, plugins, session behavior, SSO provider syncing\n- `routes/` - primary HTTP route surface\n- `routes/v2/` - newer typed route surface; pair these changes with `schemas/`\n- `routes/v2/openFoodFactsContributionRoutes.ts` - owner-only single-food preview and explicit photo-backed publication; background contributions are disabled for this release\n- `routes/v2/symptomRoutes.ts` - generic symptom tracking (`symptoms` permission): definitions (`/custom`), the pick-list library (`/options`), entries and episodes (`/entries`, `/entries/ongoing`, `/entries/:id/end`, `/entries/:id/severity`), photos, and symptom-free days. Logic lives in `services/symptomService.ts` over `models/symptomRepository.ts` and `models/symptomOptionRepository.ts`; the request/response contract is `../shared/src/schemas/api/Symptoms.api.zod.ts`\n- `routes/reportRoutes.ts` `GET /reports/training-consistency` - the consistency view (#59): `reportService.getTrainingConsistency` reads a fixed 26 weeks of exercise entries and hands them to the shared `buildTrainingConsistency`, so web and mobile show the same streak and weekly sets (`reports` permission)\n- `routes/reportRoutes.ts` `GET /reports/training-load` - acute (7 days) against chronic (4-week average) workout load (#250): `reportService.getTrainingLoad` reads every logged workout through `reportRepository.getTrainingLoadSessions` and the person's heart rate zones (`heartRateZoneService.resolveZonesForUser`), and hands them to the shared `computeTrainingLoad` (`reports` permission)\n- `routes/v2/reportRoutes.ts` - weekly alcohol rollup and the zero-padded hydration/caffeine/alcohol range used by the Trends charts (`reports` permission)\n- `routes/v2/reportRoutes.ts` `GET /compare` - does A go with B (`/api/v2/reports/compare`, owner-only via `requireSelfActor`): parses `compareQuerySchema`, `services/compareService.ts` (`getCompare`) loads both metrics through `models/compareRepository.ts` (today and the current week never counted) and hands them to the shared analysis in `../shared/src/compare/` (`stats.ts`, `metrics.ts` catalog, `analyze.ts` day/week pairing); week mode uses `utils/firstDayOfWeek.ts` and training load is weighted by the person's zones (`heartRateZoneService.resolveZonesForUser`); `mode=since` (before vs since a habit started) parses `compareSinceQuerySchema` instead and calls `getCompareSince`, which hands the shared analysis in `../shared/src/compare/since.ts` the habit over the last 730 days plus two weeks (the analysis holds a suggested start to the last 730 days, `SINCE_START_MAX_DAYS_AGO`, and offers none for a habit already kept at that limit); contract `../shared/src/schemas/api/Compare.api.zod.ts`. The AI tool `ai/tools/compareTools.ts` (`sparky_compare_metrics`, `reports` category, read-only for MCP keys) asks the same two service functions through the shared query schemas and renders the result as text; its English metric names and wording live in that file\n- `routes/v2/nutritionKineticsRoutes.ts` - active-caffeine estimate and bedtime cutoff (`diary` permission)\n- `routes/v2/workoutCoachingRoutes.ts` - adaptive coaching (#1560): session feedback (`workout_feedback`), the per-user `adaptive_workout_suggestions` setting (owner-only write), and recent-history signals (`diary` permission). `GET /v2/exercises/:id/alternatives` (ranked substitutes) lives in `routes/v2/exerciseRoutes.ts`\n- `routes/v2/routeRoutes.ts` - the person's saved routes (`/api/v2/routes`: `GET` summaries, `POST` a drawn route from `points` (+ optional `cues`), `POST /import-gpx` with the file text, `POST /from-activity/:exerciseEntryId` from a recorded GPS track, `GET/PATCH/DELETE /:id` (PATCH renames, changes the activity or replaces the line: `points` + optional `cues` go through `prepareRoute`, which recomputes distance and climb in the same UPDATE and leaves `source`, `source_exercise_entry_id` and `created_at` alone; cues need `points` and every cue index must be below the point count), `GET /:id/gpx` download; 200 per person); owner-only (`requireSelfActor`, `routes` owner policy). `models/routeRepository.ts` stores them; `utils/routePrep.ts` (`prepareRoute`) drops repeated points, measures distance and climb on the full track, thins to 2000 points and re-points cues. Geometry, GPX read/write and the contract are shared: `../shared/src/utils/routeGeometry.ts`, `routeGpx.ts`, `../shared/src/schemas/api/Route.api.zod.ts`\n- `routes/v2/routeRoutes.ts` generation endpoints - `GET /api/v2/routes/generate/capabilities` and `POST /api/v2/routes/generate` (preview only, never saves; owner-only; 10 requests per user per 10 min via `utils/userRateLimiter.ts`). Orchestration is `services/routeGenerationService.ts` (loop resizing, shared `prepareRoute` thinning); the engine sits behind `RoutingProvider` in `services/routing/`. `routingConfig.ts` `resolveRoutingProvider(userId)` picks the engine per request: the user's active `valhalla` external provider first (own, then admin-public, then family-shared via `externalProviderRepository.getActiveProviderWithKeyByType`), else none. The base URL is re-checked with the food-provider network policy and DNS-guarded for non-admin owners, the key comes from the encrypted `app_key` and is sent as `api_key`, and nothing is cached so a new provider applies at once. There are no environment variables for routing. Every engine request carries a `SparkyFitness/<version>` `User-Agent`. The type is seeded by migration `20261021120200_add_valhalla_routing_provider_type.sql` and registered in `BASE_URL_FETCHING_PROVIDER_TYPES` in `externalProviderService.ts`; `valhallaProvider.ts` talks to the engine, pure `valhallaManeuvers.ts` decodes polylines and maps maneuvers to cues, pure `loopPlanner.ts` plans loops. `GET /generate/capabilities` reports `{ available, maxAlternatives: 1, style (all false), stops: false, source: 'provider' | null }` from `getRouteGenerateCapabilities`; the server returns one route per request and `POST /generate` answers `{ route }`. Saving a generated route posts it to `POST /api/v2/routes` with `source: 'generated'` (the only source a client may choose besides `drawn`; migration `20261021120100_allow_generated_route_source.sql` widens the check). Add an engine by implementing `RoutingProvider` and a case in `routingConfig.ts`; never log the provider error object (it can carry the key).\n- `routes/v2/runProgramRoutes.ts` - the person's run program and place in it (`/api/v2/run-program`: `GET`, `PUT` to start, switch on or off, move, or set the Diary schedule (`run_days`, `start_date`, `end_date`), `POST /complete` which also records the person's local `day` as `last_done_date` and, given `entry_id`, appends `{index, entry_id, at}` to `completion_log` (newest 200 kept; cleared on a program switch) so the app can show a diary run as part of the program, `POST /effort` which saves a 1 to 10 rating for a workout already done (`rateRunProgramWorkout`: under the row lock, replaces an earlier rating of the same index, keeps the last 40 in `effort_log`, refused for a workout not yet done or another program; a program switch clears it); the tool's `get_run_program` lists recent ratings and, through shared `suggestFromEffort`, notes two hard or two easy in a row); `POST /realign` rebuilds the rest of the plan to end on the goal day through shared `realignProgram`/`applyRealign` (`../shared/src/utils/runProgramRealign.ts`): without `confirmed: true` it only returns the preview (and the options when the goal is unrealistic) and saves nothing; confirmed, it recomputes under the `adjustRunProgram` row lock (the callback also receives the whole stored `program`), refuses a stale preview `token` with 409 and an unrealistic goal without `force` with 400; the person's local `today` comes from the body or their timezone. After a realignment the stored workouts no longer line up with the built-in definition, so adjusts measure against `builtInBaseline` (built-in workouts only while the lengths match). Owner-only (`requireSelfActor`, `run_programs` owner policy). The person's own plans live in `custom_run_programs` (`GET/POST /custom`, `PUT/DELETE /custom/:id`, 30 per person, up to 364 workouts of timed steps, program id `custom:<uuid>`): starting one through `PUT` copies it into `run_programs` (with `name`/`activity`), editing one also updates the active copy and keeps the place, deleting keeps the active copy. `models/runProgramRepository.ts` seeds the stored `workouts` from the built-in program in `../shared/src/utils/runPrograms.ts`, completes the due workout in one statement so a repeat cannot skip one, and `adjustRunProgram` applies a change under a row lock with a log entry. The change rules (only upcoming workouts, running eased up to 30% or raised at most 10%) are `../shared/src/utils/runProgramAdjust.ts`, shared by the AI tool `ai/tools/runProgramTools.ts` (`sparky_manage_run_program`; every change needs `confirmed=true`; `set_run_program_schedule` changes the Diary days (names or 0 to 6), first day and goal day through the same upsert; `realign_run_program` (fields `force`, `distance`, `confirmed`) previews the same plan and applies it only with `confirmed=true`, and says nothing was changed for an unrealistic goal; `get_run_program` adds a line when the plan has drifted from the goal day; only `get_run_program` is read-only for MCP keys, and the realign POST is not on the read-only allowlist in `utils/apiKeyScope.ts`) and the contract is `../shared/src/schemas/api/RunProgram.api.zod.ts`\n- `routes/v2/heartRateZoneRoutes.ts` - the person's heart rate zone settings (`GET`/`PUT /api/v2/heart-rate-zones`; max and resting heart rate, `percent_max` / `heart_rate_reserve` / `custom` method, custom floors; `reset: true` restores defaults). Owner-only (`requireSelfActor`, also on reads). Logic in `services/heartRateZoneService.ts` over `models/heartRateZoneRepository.ts` (columns on `user_preferences`); `resolveZonesForUser(userId, observedMaxBpm?)` is what stored time in zone (`healthDataHandlers.persistWorkoutTelemetry`, `exerciseEntryService.attachWatchTelemetryToExerciseEntry`) and training load use. Zone maths is `../shared/src/utils/heartRateZones.ts`; the contract is `../shared/src/schemas/api/HeartRateZones.api.zod.ts`. Stored `exercise_entry_hr_zones` rows carry `zone_method` (`device` for Garmin) and are never recomputed when settings change.\n- `routes/v2/mindfulnessRoutes.ts` - mindfulness & meditation sessions tracking (`checkin` permission): day summaries (`/day-summary`), session logs and telemetry (`/entries`, `/entries/:id`). Logic lives in `models/mindfulnessRepository.ts`; the request/response contract is `../shared/src/schemas/api/Mindfulness.api.zod.ts`\n- `routes/v2/shoeRoutes.ts` - running-shoe inventory (`diary` permission): CRUD on `/api/v2/shoes` and `PUT /shoes/assignments/:exerciseEntryId` to link or unlink a diary entry (`exercise_entries.shoe_id`). Logic lives in `models/shoeRepository.ts`; mileage is `starting_distance_km` plus the sum of linked entries' `distance` (km); the contract is `../shared/src/schemas/api/Shoes.api.zod.ts`\n- `routes/auth/` - auth-specific route fragments mounted through `routes/authRoutes.ts`\n- `services/` - business logic and orchestration\n- `models/` - PostgreSQL repositories and persistence helpers\n- `middleware/` - auth, permissions, uploads, and shared Express middleware\n- `utils/uploadsPath.ts` - the uploads root plus the resolver and containment guard for stored `file_path` values; use it instead of re-deriving `SPARKY_FITNESS_CUSTOM_UPLOADS_DIRECTORY`\n- `utils/oauthState.ts` - server-issued single-use OAuth `state` nonces for provider linking (`issueOAuthState`, `persistOAuthState`, `claimOAuthState`); use it instead of hand-rolling a state value\n- `utils/outboundHttp.ts` - process-wide outbound HTTP defaults, applied from `index.ts`: the axios request timeout and the per-address-family connect attempt timeout (`net.setDefaultAutoSelectFamilyAttemptTimeout`). Both are fixed constants on purpose - do not add env overrides, and read the sizing note there before changing either, because the two values interact\n- `utils/errors.ts` - `ValidationError` plus `describeError(error)`; prefer it over `error.message` when logging any caught value, because an `AggregateError` or a non-Error throw renders as an empty string\n- `middleware/requireSelfMiddleware.ts` - `requireSelfActor`, which rejects a switched/delegated context outright; attach per-route to account-linking routes\n- `integrations/` - provider adapters and ingest pipelines\n- `schemas/` - Zod route schemas\n- `types/` - TypeScript declarations, including `Express.Request` augmentation\n- `db/` - pool management, grants, migrations, and RLS policies\n- `config/` - logging and Swagger config\n- `utils/` - startup helpers, CORS, permissions, timezone loading, OIDC helpers, migration helpers\n- `ai/` - AI provider configuration (`config.ts`), the unified provider-dispatch helper (`providerDispatch.ts`), and the in-process chatbot tool registry (`ai/tools/`)\n- `security/` - encryption utilities (`encryption.ts`)\n- `validation/` - legacy express-validator rules for a few older routes (new routes use Zod schemas)\n- `constants/` - shared constants and supporting package data\n- `tests/` - Vitest suites plus a few utility scripts\n- `devdocs/` - local notes and debugging artifacts when present\n\nWhen searching, ignore noisy/generated directories unless you explicitly need them:\n\n- `node_modules/`\n- `coverage/`\n- `uploads/`\n- `temp_uploads/`\n- `backup/`\n- `mock_data/`\n\n## Architecture\n\n### Boot and App Shell\n\n- `index.ts` is the true local boot path used by `pnpm start`; do not bypass it for normal development because it performs env loading and preflight work\n- `SparkyFitnessServer.ts` creates the Express app, configures static upload serving, mounts auth interception, registers routes, exposes API docs, starts background jobs through `services/backgroundJobScheduler.ts`, and handles graceful shutdown\n- Startup order matters:\n  - `index.ts`: await `initializeDatabase()`, which applies pending migrations and then reapplies `db/rls_policies.sql` under a PostgreSQL advisory lock, **before** importing `SparkyFitnessServer.ts` (and therefore `auth.ts`)\n  - upsert env-configured OIDC provider\n  - mount Better Auth\n  - sync trusted SSO providers\n  - register cron jobs\n  - optionally promote `SPARKY_FITNESS_ADMIN_EMAIL` to admin\n  - start listening\n- Public API docs live at:\n  - `/api/api-docs/swagger`\n  - `/api/api-docs/redoc`\n  - `/api/api-docs/json`\n- If you change public endpoints, keep Swagger JSDoc and `config/swagger.ts` coverage accurate\n\n### Environment and Secrets\n\n- Runtime `.env` is expected at `../.env`\n- The tracked template lives at `../docker/.env.example`\n- `utils/secretLoader.ts` loads `*_FILE` secrets before preflight validation\n- Three layers decide whether a variable has to be set, and they are easy to confuse:\n  1. **`utils/preflightChecks.ts` refuses to start** without these four, because none has a safe default:\n     - `SPARKY_FITNESS_DB_PASSWORD`\n     - `SPARKY_FITNESS_FRONTEND_URL`\n     - `SPARKY_FITNESS_API_ENCRYPTION_KEY`\n     - `BETTER_AUTH_SECRET`\n  2. **`preflightChecks.ts` fills in a default** for `SPARKY_FITNESS_DB_HOST` (`sparkyfitness-db`), `SPARKY_FITNESS_DB_NAME` (`sparkyfitness_db`), `SPARKY_FITNESS_DB_USER` (`sparky`) and the two app-role variables, logging which one it defaulted. These matter only outside Compose, which supplies them itself.\n  3. **`docker/docker-compose.prod.yml` supplies a value** for almost everything via `${VAR:-default}`, so a Compose deployment only ever has to set the four in (1). Keep the defaults in (2) identical to Compose's: a value that differs between them silently points the server at a database other than the one Compose created.\n- `SPARKY_FITNESS_APP_DB_USER` and `SPARKY_FITNESS_APP_DB_PASSWORD` are soft-required: preflight defaults the user to `sparky_app` and mints a password when absent, and `utils/dbMigrations.ts` creates the role or re-syncs its password so the two always match. It probes a connection as that role first, so an externally pre-created role is left alone and the owner does not need `CREATEROLE` — but only while `SPARKY_FITNESS_APP_DB_PASSWORD` still authenticates. If it is absent, preflight mints a new one, the probe fails, and the `ALTER ROLE` does need `CREATEROLE`; an externally managed database should therefore set both app variables explicitly. The probe only reports failure for an authentication rejection (`28P01`/`28000`); any other connection error propagates rather than being misread as a stale password. Both assignments must stay in `preflightChecks.ts`, because `db/poolManager.ts` freezes its credentials at module load\n- `BETTER_AUTH_SECRET` is mandatory. It signs session cookies and encrypts stored 2FA/TOTP secrets, so a value that changes between restarts logs every user out and permanently locks out anyone with 2FA enabled. Startup used to mint a throwaway one when it was missing, which made exactly that happen silently; it now fails preflight instead\n- Preflight also refuses to start when `BETTER_AUTH_SECRET` or `SPARKY_FITNESS_API_ENCRYPTION_KEY` still holds a template placeholder (a value starting with `changeme` or `replace_with`, as shipped in `docker/.env.example` and `docker/.env.simple.example`), and only warns for `SPARKY_FITNESS_DB_PASSWORD` because Compose initialises Postgres with it. Keep new template secrets on one of those prefixes so the check covers them. It also fails when `BETTER_AUTH_SECRET` base64-decodes to an empty key (Better Auth accepts an empty Buffer) and warns under 32 decoded bytes\n- Common operational toggles include `SPARKY_FITNESS_SERVER_PORT`, `SPARKY_FITNESS_ADMIN_EMAIL`, `ALLOW_PRIVATE_NETWORK_CORS`, `ALLOW_PRIVATE_NETWORK_AI`, `ALLOW_PRIVATE_NETWORK_FOOD_PROVIDERS`, `SPARKY_FITNESS_EXTRA_TRUSTED_ORIGINS`, and `BETTER_AUTH_URL`\n- User-configured self-hosted food providers (Mealie/Tandoor/Norish) can point `base_url` at a private/internal address only for admins by default; a non-admin on a multi-user server is blocked unless the operator opts in, either with the admin `allow_private_network_food_providers` toggle (Admin > Global Provider Settings) or `ALLOW_PRIVATE_NETWORK_FOOD_PROVIDERS=true`. This mirrors the AI policy (a single-user self-host is an admin, so their LAN recipe server works with no config). Enforced by `utils/outboundUrlPolicy.ts` at provider save time in `services/externalProviderService.ts`. Separate from the AI toggle by design\n- The admin `allow_private_network_ai` toggle (Admin > Global AI Settings), or `ALLOW_PRIVATE_NETWORK_AI=true`, lets non-admin users use custom AI service URLs (`custom`/`ollama`/`openai_compatible`) that resolve to private/internal addresses; default off is an SSRF guard enforced by `utils/outboundUrlPolicy.ts` at save/test time and again in the runtime guarded fetch path. Current admins and global admin-created AI settings can use private URLs for self-hosted providers like Ollama\n- **Call `resolveAiNetworkPolicy` / `resolveFoodProviderNetworkPolicy`, not the `derive*` forms.** The sync `derive*` functions only see the env var; the async `resolve*` wrappers also consult the admin toggle (and only hit the database when the sync answer would be a denial). The `derive*` exports stay for unit tests and for the resolvers themselves\n\n### TypeScript and Module Conventions\n\n- This package is now almost entirely TypeScript; new source files should be `.ts`\n- Keep local relative imports using `.js` extensions from TypeScript files, for example `import foo from './foo.js'`\n- `eslint.config.js` enforces file extensions in imports\n- `tsconfig.json` uses `NodeNext`, `noEmit`, and `allowJs: false`\n- `@workspace/shared` resolves directly to `../shared/src/index.ts` here and in Vitest\n- Avoid using `any` declarations in models, repositories, and integration services (e.g. `integrations/fatsecret/fatsecretService.ts`). Instead, use base datatypes (like `string`), proper types/interfaces, or import strict type schemas directly from `@workspace/shared`.\n- New public endpoints should include TypeScript code, Zod validation, and automated tests\n\n### Logging\n\n- Use `log(level, message, ...args)` from `config/logging.ts`; levels are `'debug'`, `'info'`, `'warn'`, and `'error'`\n- Never use `console.error` (or other `console.*`) in application code\n- Exception: fatal boot diagnostics in `index.ts` and `utils/preflightChecks.ts` print with `console.error` (alongside `log('error', ...)`), because `log()` is suppressed at `SILENT` and the operator must still see why the server refused to start\n- `SPARKY_FITNESS_LOG_LEVEL` controls verbosity (`DEBUG`, `INFO`, `WARN`, `ERROR`, `SILENT`)\n\n### Database and RLS\n\n- Use `getClient(userId, authenticatedUserId?)` from `db/poolManager.ts` for normal user-scoped queries\n- `getClient(...)` sets `public.set_app_context(...)`; that is what makes row-level security work correctly\n- Use `getSystemClient()` only for admin, migration, startup, or policy-management work that intentionally bypasses RLS\n- Always release database clients in a `finally` block\n- To learn a table's current shape, read `../shared/src/schemas/database/<Table>.zod.ts` (one small Zod file per table) instead of reading `../db_schema_backup.sql` or reconstructing it from the 185 migration files\n- New migrations belong in `db/migrations/` and must use `YYYYMMDDHHMMSS_description.sql`\n- **Never manually edit `../db_schema_backup.sql`** — after merge, CI regenerates it from the migrations and opens an automated sync PR (`.github/workflows/schema-backup.yml`). Do not commit copies generated from a local database.\n- If you add a new table or change user-visible access behavior, follow `../agent-docs/new-migration-checklist.md`. In short, you MUST:\n  1. Add/modify the RLS policies in `db/rls_policies.sql`.\n  2. Update the user-facing documentation in `../docs/src/features/family-friends-sharing.md`.\n  3. Update the developer-facing documentation in `../docs/src/developer/database-security-tiers.md` to define its security tier (Tier 1, Tier 2, or Tier 3).\n  4. Add or update the matching Zod schema in `../shared/src/schemas/database/`.\n- Keep future schema-startup steps in `utils/initializeDatabase.ts` and pass its shared client through all database work. The lock and schema work must use the same connection so initialization cannot continue on another connection after the lock-owning session is lost. Do not create alternate migration mechanisms.\n- Migrations run from `index.ts`, **before any application module is imported**, and via dynamic `await import()`. Both details are load-bearing: Better Auth validates the schema eagerly at `auth.ts` module scope and caches a mismatch for the life of the process (issues #2469 / #2470), and `db/poolManager.ts` builds its pools at module load, so a static import would be hoisted above the env/secret loading. `tests/bootOrder.test.ts` guards this\n\n### Uploads: Public vs Sensitive\n\n- `SparkyFitnessServer.ts` serves the uploads root publicly at `/uploads` and `/api/uploads`; both are in `publicRoutes`, so `authenticate` never runs on them\n- Sensitive subtrees are **denied on the static mount** and served instead by an authenticated, owner-checked per-id route. Three exist today:\n  - `check-in` -> `GET /api/measurements/check-in-photos/file/:id` (delegatable via the `checkin` permission)\n  - `pregnancy` -> `GET /api/v2/pregnancy/photos/file/:id` (owner-only; deliberately **no** `checkPermissionMiddleware`, because reproductive-health data is never delegated)\n  - `symptoms` -> `GET /api/v2/symptoms/photos/file/:id` (delegatable via the `symptoms` permission)\n- Adding a sensitive upload subtree means adding its directory name to `SENSITIVE_UPLOAD_SUBTREES` in `SparkyFitnessServer.ts` **and** adding an authenticated file route; the deny rule matches the decoded, normalized path, because a prefix match on the raw URL is bypassable with `..%2f`\n- Responses for these domains omit `file_path`: the on-disk layout is a server detail and clients address photos by id\n- `tests/uploadsStaticMount.test.ts` guards both the deny behavior and the fact that the deny rule is registered before `express.static`\n\n### Auth and Request Context\n\n- Better Auth is configured in `auth.ts` and mounted under `/api/auth`\n- `SparkyFitnessServer.ts` intercepts `/api/auth*` requests before the normal request logger and has special handling for discovery routes and sign-out cookie cleanup\n- `middleware/authMiddleware.ts` populates:\n  - `req.userId`\n  - `req.authenticatedUserId`\n  - `req.originalUserId`\n  - `req.activeUserId`\n  - `req.user`\n- `req.userId` is the active RLS target; `req.authenticatedUserId` is the logged-in actor\n- Family and delegated access flow through `middleware/checkPermissionMiddleware.ts`, `middleware/onBehalfOfMiddleware.ts`, and the auth middleware’s active-user switching\n- `checkPermissionMiddleware(permissionType)` guards routes; permission types are `'diary'`, `'reports'`, `'checkin'`, `'medications'`, and `'symptoms'` (GET resolves to the read-only `*_read` variant)\n- If you change auth behavior, check both cookie-backed sessions and API key flows\n- API keys carry a scope in `api_key.permissions` (Better Auth format, `@workspace/shared` `ApiKeys.api.zod.ts`): `{ sparky: [\"read\"] }` is read-only, `{ sparky: [\"read\", \"write\"] }` or no permissions is full access. The scope is set only by `POST /api/identity/user/generate-api-key`, because Better Auth refuses `permissions` from a client. `authenticate` sets `req.apiKeyReadOnly` and answers 403 for anything but GET/HEAD/OPTIONS plus the POST allowlist in `utils/apiKeyScope.ts`; the `/api/auth` interceptor refuses mutations from read-only keys via `middleware/readOnlyApiKeyGuard.ts`; `/mcp` filters tools and actions with `ai/mcp/toolAccess.ts`. A new MCP tool or action is unavailable to read-only keys until it is classified there\n\n### Dates, Day Strings, and Timezones\n\n- Prefer the shared helpers exported by `@workspace/shared` for day-string and timezone-aware logic\n- Common server-side helpers include `todayInZone`, `instantToDay`, `instantForDay`, `dayToUtcRange`, `dayRangeToUtcRange`, `localDateToDay`, `addDays`, `compareDays`, and `isDayString`\n- Load the user timezone through `utils/timezoneLoader.ts` before deriving \"today\", bucketing events by day, or building date ranges from user context\n- Treat `YYYY-MM-DD` values as calendar-day strings, not UTC-midnight timestamps\n- Avoid `toISOString().split('T')[0]` for user-facing or business-logic dates; it silently shifts dates near timezone boundaries\n- If you touch older code that still uses UTC split patterns, prefer migrating that path to the shared helpers instead of copying the pattern forward\n- Timezone/date regression coverage already exists in:\n  - `tests/timezone.test.ts`\n  - `tests/dateShifting.test.ts`\n  - `tests/measurementService.timezone.test.ts`\n\n### Integrations and Background Work\n\n- Provider-specific adapters live under `integrations/`; coordinating logic usually lives in `services/` and persistence in `models/`\n- Current adapters span food/nutrition (OpenFoodFacts, FatSecret, Nutritionix, USDA, Mealie, Tandoor, Norish, SwissFood, Yazio), fitness devices (Garmin Connect sync plus FIT file import via `integrations/garminfit/` + `services/fitImportService.ts`, Withings, Fitbit, Oura, Polar, Strava, COROS, Hevy), exercise databases (Wger, FreeExerciseDB), and health-data import (Google Health, generic/mobile health data)\n- Scheduled jobs currently include backups, session cleanup, and hourly sync loops for Withings, Garmin, Fitbit, Oura, Polar, Strava, and COROS\n- Integration work often spans route, service, repository, cron, and external-provider settings code; inspect the whole path before calling the work complete\n- **OAuth linking (`/authorize`, `/callback`) is self-only, and `state` is a server-issued single-use nonce.** Never derive a user id from a callback request body, and never gate an authorize route with `checkPermissionMiddleware('diary')` — on GET that resolves to `diary_read`, which would hand a read-only delegate the owner's decrypted OAuth client id. Use `requireSelfActor` plus `utils/oauthState.ts`. Withings and Polar follow this pattern; Oura, Fitbit and Strava are self-only but still send `state = userId` and ignore it on callback (tracked follow-up)\n\n### AI Services\n\n- AI calls go through the Vercel `ai` SDK (v7) with provider adapters for OpenAI, Anthropic, and Google, plus OpenAI-compatible, Mistral, Groq, OpenRouter, and Ollama service types\n- `ai/config.ts` holds default model and vision-model selection per provider; `ai/providerDispatch.ts` is the unified dispatch helper used by chat, food-photo analysis, nutrition-label scan, and unit conversion\n- Prefer routing new AI features through `providerDispatch.ts` instead of calling provider SDKs directly\n- Web and mobile voice transcription use `POST /api/chat/transcribe` in `routes/chatRoutes.ts` and `services/voiceTranscriptionService.ts`; its multipart fields, response, MIME types, and 10 MB limit come from `shared/src/schemas/api/VoiceTranscription.api.zod.ts`. Audio stays in memory, uses actor-scoped AI settings with `supports_audio_input`, and all outbound calls use `createGuardedFetch`. Voice and chat providers are selected independently; transcription never executes chatbot tools or writes diary entries. `voice_input_enabled` and `active_voice_ai_service_id` in user preferences synchronize voice UI across clients; null selects System recognition, and disabling voice preserves the selection. Preference writes validate these fields using the shared voice-preferences schema.\n- Chatbot tool calls run in-process through the registry in `ai/tools/`\n- `ai/tools/index.ts` exposes `buildChatbotTools(userId, tz)`, composing the per-domain builders (`build<Domain>Tools` in `ai/tools/<domain>Tools.ts`); handlers close over the authenticated user — so two-actor services receive `(userId, userId, ...)` — and the user's IANA timezone, used for \"today\" defaults and day bucketing\n- Tool handlers follow a fixed contract: publish a flat Zod schema, validate with a strict union `safeParse` inside `execute`, orchestrate through existing services and repositories, and never throw - errors come back as `ERRORS.*` strings from `ai/tools/errors.ts`\n- Tool output text is a parity contract with the MCP tool set; golden tests in `tests/chatbotTools*.test.ts` assert exact returned strings, so do not reword tool output casually\n\n## Testing and Validation\n\n- Test runner: Vitest, not Jest\n- Auto-discovered test files match `tests/**/*.test.ts`\n- `tests/check_routes.ts` and `tests/*.script.ts` are utility scripts, not normal test suites\n- For route or contract work, targeted `supertest`-based Vitest tests are the normal validation path\n- Prefer `pnpm run typecheck` after touching `routes/v2/`, `schemas/`, `types/`, or shared request/response contracts\n- Prefer `pnpm run lint` after multi-file edits; if unrelated package-wide issues make that noisy, run targeted `pnpm exec eslint <paths>` on the touched files before stopping\n- Use `pnpm run test:coverage` after broad service, route, repository, middleware, or auth refactors\n\n## Quick Routing\n\n- Weekly weight-change calorie goals: `services/preferenceService.ts` validates the signed `goal_mode_weekly_change_kg` preference (negative loses, positive gains; active only for `goal_mode = 'weekly'`), `models/preferenceRepository.ts` persists it, and `services/goalService.ts` plus `services/calorieBalanceService.ts` pass it to the shared calorie adjustment functions. Device Projection applies the daily weekly-rate adjustment once to device TDEE; percentage goals keep their existing behavior.\n\n- Startup, env, or deployment issue:\n  inspect `index.ts`, `SparkyFitnessServer.ts`, `utils/secretLoader.ts`, `utils/preflightChecks.ts`, and `config/logging.ts`\n- Auth, session, MFA, or API key issue:\n  inspect `auth.ts`, `middleware/authMiddleware.ts`, `routes/authRoutes.ts`, and `routes/auth/`\n- Migration, RLS, or permission issue:\n  inspect `db/migrations/`, `db/rls_policies.sql`, `db/poolManager.ts`, `utils/applyRlsPolicies.ts`, and the permission middleware/helpers\n- Public v2 contract issue:\n  inspect the matching file in `routes/v2/` plus the related Zod schema in `schemas/`\n- Food, barcode, or external provider issue:\n  inspect the relevant `integrations/*` code, then the matching service and repository files\n- Open Food Facts publication:\n  inspect `services/openFoodFactsManualContributionService.ts`, `integrations/openfoodfacts/openFoodFactsContribution.ts`, and `constants/openFoodFacts.ts`; retained automatic queue code is dormant and needs a new migration before a future release can activate its triggers\n- Heart rate zone, max heart rate or time-in-zone issue:\n  inspect `../shared/src/utils/heartRateZones.ts`, `services/heartRateZoneService.ts`, `routes/v2/heartRateZoneRoutes.ts`, then `services/healthDataHandlers.ts` (`persistWorkoutTelemetry`) and `services/exerciseEntryService.ts` (watch telemetry) for the write paths\n- Health data or date bucketing issue:\n  inspect `integrations/healthData/healthDataRoutes.ts`, `services/measurementService.ts`, and `utils/timezoneLoader.ts`\n- Attaching heart rate to an already-existing exercise entry (e.g. from a paired Apple Watch's live workout tracking, `SparkyFitnessMobile/src/hooks/useWatchWorkoutBridge.ts`):\n  inspect `POST /exercise-entries/:id/watch-telemetry` in `routes/exerciseEntryRoutes.ts`, `services/exerciseEntryService.ts`'s `attachWatchTelemetryToExerciseEntry`, and `models/exerciseEntry.ts`'s `applyWatchTelemetryAtomically`. Distinct from `services/healthDataHandlers.ts`'s `persistWorkoutTelemetry`, which creates a new entry as part of importing a whole synced workout (HealthKit/Health Connect/Garmin) — this route only fills in avg/max HR, `exercise_entry_hr_zones` and `calories_burned` on an entry that already exists, and it also stores the posted series as `heart_rate` samples tagged with the entry (`storeHeartRateSeries`, source `SparkyFitness Watch`, `merge` over the workout's own span but only this entry's samples, and only after that snapshot was accepted under the row lock, best effort) because the heart rate chart of an entry with no GPS track reads those samples and would otherwise wait on a Health sync. `activeEnergyKcal` is a real measurement from the watch and is written to BOTH `calories_burned` (the figure the diary totals) and `active_calories` (a telemetry column the ordinary entry update preserves). That second write is what makes the measurement survive a later edit: `resolveEditedCaloriesBurned` in `services/exerciseService.ts` prefers a stored `active_calories` over the recomputed duration-and-sets estimate, so editing a note or a weight no longer replaces what a watch measured with a formula. A client-sent `calories_burned` still overrides both. Both body fields are individually optional (HealthKit permissions are per type) and the model does a partial UPDATE, so a calories-only post must not blank heart rate an earlier post attached.\n  Workout effort (a 1-10 rating of the whole workout, `exercise_entries.workout_effort`, CHECK 1-10): it is one of `EXERCISE_ENTRY_TELEMETRY_COLUMNS`, so a synced workout carries it as `telemetry.workout_effort` through the health-data allowlist (HealthKit sends the person's own Apple rating) and the model's insert, update and select pick it up with no per-column code. `POST /exercise-entries/:id/effort` (`setExerciseEntryEffort`, body `{rating}`) is the write for ratings given afterwards, on the watch: it sets the entry and every other entry with the same `exercise_preset_entry_id`, so a strength session reads the same from any exercise and the phone needs only one entry id. `GET /api/exercise-stats/query` returns it as `workoutEffort`.\n- Attaching a phone-recorded GPS track to an existing exercise entry (mobile `RecordActivityScreen`):\n  inspect `POST /exercise-entries/:id/gps-track` in `routes/exerciseEntryRoutes.ts` and `services/exerciseEntryService.ts`'s `attachGpsTrackToExerciseEntry`, with the request schema `attachExerciseEntryGpsTrackRequestSchema` in `shared/src/schemas/api/ExerciseEntries.api.zod.ts`. It stores the route and laps (laps arrive as bare windows and are derived by `deriveLaps`) and backfills speed/elevation/moving-time with `deriveWorkoutTelemetry`, all in one transaction. Both inserts upsert per entry, so a retried post replaces the track. Use this rather than `/api/health-data` for app-recorded activities: the health-data path range-deletes a source's entries for the affected days.\n- Attaching laps to an entry with no GPS track (an indoor run recorded on the watch):\n  inspect `POST /exercise-entries/:id/laps` in `routes/exerciseEntryRoutes.ts` and `services/exerciseEntryService.ts`'s `attachLapsToExerciseEntry`, with `attachExerciseEntryLapsRequestSchema` in `shared/src/schemas/api/ExerciseEntries.api.zod.ts`. The client sends the marked windows with the distance and average heart rate it worked out; the server derives duration and speed and upserts by lap index into `exercise_entry_laps`, so a retry replaces them. Entries with a track get their laps from `/gps-track` instead.\n  For an indoor run's pace chart, `POST`/`GET /exercise-entries/:id/distance-series` (`attachDistanceSeriesToExerciseEntry`, `getExerciseEntryDistanceSeries`, table `exercise_entry_distance_series`, rows of `[active seconds, metres]`; `setExerciseEntryDistance` rescales them with the laps).\n- Correcting the distance of an entry with no GPS track (a treadmill run, from the summary screen):\n  inspect `POST /exercise-entries/:id/distance` in `routes/exerciseEntryRoutes.ts`, `services/exerciseEntryService.ts`'s `setExerciseEntryDistance` and `setExerciseEntryDistanceRequestSchema` in `shared/src/schemas/api/ExerciseEntries.api.zod.ts`. It changes only the entry's `distance`, its sets' distance and any laps (`distance_meters` and speed scaled by new over old distance), in one transaction, so measured heart rate and energy are kept; an entry with a GPS track is refused with 409.\n- Water, hydration, caffeine, or alcohol issue:\n  inspect `services/hydrationTotalsService.ts` (the single owner of the daily water formula), `services/measurementService.ts` (the container \"+/-\" path and the container->food link), `services/caffeineKineticsService.ts` / `services/alcoholWeekService.ts`, `models/waterContainerRepository.ts`, and the shared maths in `../shared/src/nutrients/`\n- Self-service \"delete synced data by source\" issue:\n  inspect `routes/syncedDataRoutes.ts`, `services/syncedDataService.ts`, and `models/syncedDataRepository.ts` (the `SYNCED_SOURCE_TABLES` whitelist)\n- AI chat or chatbot tool issue:\n  inspect `services/chatService.ts`, `ai/tools/`, and the matching domain service and repository\n- Fasting or mood issue:\n  inspect `routes/fastingRoutes.ts` / `routes/moodRoutes.ts` and `models/fastingRepository.ts` / `models/moodRepository.ts`\n  `mood_entries` holds several rows per day, each with `entry_timestamp` and a `source`: `manual` (web/app check-ins, AI `log_mood`, end-of-fast mood), or `garmin` / `import` (derived, one row per source per day via a partial unique index). `mood_value` is nullable; a CHECK needs at least one of mood, energy, focus, notes or tags (whitespace-only notes and blank tags do not count, as in `hasMoodContent`), and another limits `source` to those three values. Requests without the `X-Mood-Model-Version` header keep the one-mood-per-day contract on the \"legacy row\" (latest manual row with a mood, falling back to the derived row on GET, Garmin before import); `X-Mood-Model-Version: 2` always adds a check-in and reads arrays (header is in `CORS_ALLOWED_HEADERS`, `utils/corsHelper.ts`); its PUT edits only manual rows (409 for Garmin and import) and re-dates a check-in only when its timestamp actually changes. A check-in or derived mood written without a time of its own gets `instantForDay` from `@workspace/shared`: now for today, local noon for another day, so `entry_timestamp` stays on `entry_date`. Per-day aggregates (coach, cycle correlations) use `DAILY_MOOD_VALUE_SQL`: rounded average of manual moods, else Garmin, else import.\n- Symptom or episode tracking issue (#1882):\n  inspect `routes/v2/symptomRoutes.ts`, `services/symptomService.ts`, `models/symptomRepository.ts`, `models/symptomOptionRepository.ts`, and the shared contract and built-ins in `../shared/src/schemas/api/Symptoms.api.zod.ts` and `../shared/src/symptoms/`. `symptom_entries` also holds cycle-hub symptoms (`source = 'cycle'`), which RLS keeps owner-only\n- Medications, cycle, or pregnancy issue:\n  inspect the matching v2 route (`routes/v2/medicationRoutes.ts`, `routes/v2/cycleRoutes.ts`, `routes/v2/pregnancyRoutes.ts`), its Zod schema in `schemas/`, then `services/cycleService.ts` / `services/pregnancyService.ts` and the `models/medication*Repository.ts` / `models/cycleRepository.ts` / `models/pregnancyRepository.ts` files\n- Supplement barcode lookup:\n  `GET /api/v2/medications/supplement-lookup?upc=` (registered before `/:id`) runs only while the user can see an active `dsld` or `openfoodfacts` external provider (`getActiveProvidersByTypes`; neither is a 404; the NIH database answers first and `services/supplementOpenFoodFactsService.ts` asks Open Food Facts, per-serving nutrients only, when it has no match or `dsld` is off; migration `20261006200000_add_dsld_provider_type.sql`, constant `SUPPLEMENT_LOOKUP_PROVIDER_TYPE`) and calls `services/supplementLookupService.ts`. Two more routes read a Supplement Facts photo: `POST /supplement-label/scan` runs the user's vision AI (`services/supplementLabelScanService.ts`, same provider and error map as `/foods/scan-label`) and `POST /supplement-label/map` takes ingredients the phone read on device; both end in `mapScannedLabel`, so name matching and unit conversion are shared with the barcode lookup, which searches the NIH Dietary Supplement Label Database for the code, opens the hits to check the label's own UPC, and maps the label to one serving's nutrients in the app's units (IU for vitamins A, D and E converted, compounds and unplaceable ingredients returned as `unmatched`). The response contract is `shared/src/schemas/api/SupplementLookup.api.zod.ts`; an unreachable database is a 502, no match is `{ product: null }`.\n- Exercise alternatives, workout feedback, or adaptive suggestions issue (#1560):\n  inspect `services/exerciseAlternativesService.ts` (library + Free Exercise DB candidates, dedupe) with the pure ranking in `utils/exerciseAlternativesRanking.ts` and the muscle/equipment vocabulary in `../shared/src/constants/exerciseTaxonomy.ts`; feedback in `services/workoutCoachingService.ts` + `models/workoutFeedbackRepository.ts`; signals in `services/adaptiveWorkoutService.ts`. The rules that turn signals into weight changes are client-side and shared (`../shared/src/utils/adaptiveCoaching.ts`) so web, mobile and the AI tools agree; the server only reports what happened. AI actions: `suggest_alternatives`, `rate_workout`, `get_workout_coaching` in `ai/tools/exerciseTools.ts`\n- Bodyweight exercise load, volume or 1RM issue (#56):\n  a `bodyweight_reps` set's weight is signed added/assisting load, and its load is body weight plus that weight (`effectiveLoadKg` in `../shared/src/utils/exerciseLoad.ts`). SQL mirrors the rule in `utils/exerciseLoadSql.ts` (`bodyWeightJoinSql`, `setLoadSql`), used by `services/exerciseStatsService.ts` and `models/exerciseEntry.ts`'s progress query; `services/reportService.ts` applies the shared helper with readings from `reportRepository.getBodyWeightReadings`. Keep the two copies identical. A negative set weight is rejected unless that exercise's modality is `bodyweight_reps`. `models/exercise.ts`'s `createExercise` defaults the modality from equipment through `resolveExerciseModality`\n  `weight_distance` (weighted carries) and `weight_duration` (loaded holds) store weight plus distance (km) or duration (s); stats treat them as weight-based, not cardio, and best-set ordering falls back to distance then duration.\n- Sleep or sleep-science issue:\n  inspect `routes/sleepRoutes.ts`, `routes/sleepScienceRoutes.ts`, `services/sleepAnalyticsService.ts`, `services/sleepScienceService.ts`, and the sleep repositories\n\n## Architecture Resources\n\nBefore adding a feature or changing auth/permission behavior, read:\n\n- `../docs/src/developer/database.md` — Quick table index (all ~120 tables with purpose) + migration best practices\n- `../docs/src/developer/database-security-tiers.md` — Security tier, permission type, and RLS rules for every table (authoritative)\n- `../agent-docs/architecture-permissions.md` — Permission types, links to tier classification doc\n- `../agent-docs/data-flow-patterns.md` — Data flow from frontend through server to database, safe RLS patterns\n- `../agent-docs/new-domain-template.md` — Checklist for adding a major feature domain\n- `../agent-docs/anti-patterns.md` — Common mistakes (using getSystemClient(), forgetting RLS, cache invalidation, timezone bugs, cross-package contract mismatches)\n\n## Working Rules\n\n- Match the existing service/repository/middleware layering instead of introducing parallel abstractions\n- **Library Deletes vs Diary Snapshots:** `exercise_entries` and `food_entries` are snapshot-backed (`exercise_id` / `food_id` are `ON DELETE SET NULL`). `deleteExercise` and `deleteFood` (`mode: 'delete'`) must never delete past or today's diary entries; they cascade from templates/presets, clean up future scheduled plan entries (`entry_date >= today AND workout_plan_assignment_id IS NOT NULL`), and clean up empty parent preset entries. Only explicit `delete_with_history` (force delete) deletes diary entries for that user. If an item is referenced by others (`otherUserReferences > 0`), the delete must fall back to `hide` (`is_quick_exercise` / `is_quick_food`).\n- If your change adds a new domain, route family, or table, update this file's Snapshot, Source Map, and Quick Routing sections (and the `Last updated` date) in the same change\n- If you add persisted or user-visible data, think through migration, RLS, permissions, tests, API docs, and downstream client contracts together\n- Validate shared-contract","SparkyFitnessMobile/CLAUDE.md":"See @AGENTS.md\n","CLAUDE.md":"See @AGENTS.md\n","SparkyFitnessServer/CLAUDE.md":"# SparkyFitnessServer - Claude Agent Guidelines & System Prompt\n\nPath: `SparkyFitnessServer/CLAUDE.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/CodeWithCJ/SparkyFitness/HEAD/SparkyFitnessServer/CLAUDE.md)"},"files":{".claude/skills/new-migration/SKILL.md":"---\nname: new-migration\ndescription: Use whenever adding or changing a SparkyFitness database migration, creating a new table, or altering user-visible data access. Walks the mandatory cross-package checklist (RLS policies, schema backup sync, shared Zod schema, docs security tiers, downstream contracts). Triggers on \"new table\", \"migration\", \"ALTER TABLE\", \"RLS\", \"schema change\".\n---\n\n# New Migration\n\nSee @.agents/skills/new-migration/SKILL.md — that is the canonical skill body. Read it and follow it.\n",".github/copilot-instructions.md":"# SparkyFitness AI Coding Agent Instructions\n\n## Architecture Overview\n\n**Full-stack fitness tracking application** - self-hosted alternative to MyFitnessPal with AI-powered nutrition assistance.\n\n- **Frontend**: React 18 + TypeScript + Vite (`src/`)\n- **Backend**: Node.js/Express (`SparkyFitnessServer/`)\n- **Database**: PostgreSQL with Row Level Security (RLS)\n- **UI**: Tailwind CSS + shadcn/ui components\n- **State**: React Context + TanStack Query\n- **AI**: Multi-provider support (OpenAI, Anthropic, Google, etc.)\n\n## Development Workflow\n\n### Running the Application\n```bash\n# Frontend development server (port 8080)\nnpm run dev\n\n# Backend server (port 3010)  \nnpm run start-backend\n\n# Docker development (local builds)\n./docker/docker-helper.sh dev up\n\n# Docker production (DockerHub images)\n./docker/docker-helper.sh prod up\n```\n\n### Key Configuration Files\n- **Vite config** (`vite.config.ts`): API proxy setup (`/api` → `localhost:3010`)\n- **Environment**: Root `.env` file (loaded by backend at `path.resolve(__dirname, '../.env')`)\n- **Docker**: `docker/` directory contains organized Docker configuration\n  - `docker-compose.dev.yml` - Local development builds with volume mounts\n  - `docker-compose.prod.yml` - Production DockerHub images\n  - `docker-helper.sh` - Comprehensive management script with help system\n  - `nginx.conf.template` - Parameterized nginx configuration with envsubst\n  - `.env.example` - Template for environment variables\n\n### Docker Helper Script\nThe `docker-helper.sh` script provides easy management:\n```bash\n# Show all available commands\n./docker/docker-helper.sh help\n\n# Development environment\n./docker/docker-helper.sh dev up     # Start all services\n./docker/docker-helper.sh dev logs   # View logs\n./docker/docker-helper.sh dev clean  # Clean volumes and images\n\n# Production environment  \n./docker/docker-helper.sh prod up    # Start production services\n./docker/docker-helper.sh prod down  # Stop and remove containers\n```\n\n## Critical Architecture Patterns\n\n### Repository Pattern (Backend)\nAll database operations use repository pattern in `SparkyFitnessServer/models/`:\n```javascript\n// Example: userRepository.js\nconst pool = require('../db/connection');\n\nasync function createUser(userId, email, hashedPassword, full_name) {\n  const client = await pool.connect();\n  try {\n    await client.query('BEGIN');\n    // Multiple related inserts in transaction\n    await client.query('COMMIT');\n  } catch (error) {\n    await client.query('ROLLBACK');\n    throw error;\n  } finally {\n    client.release();\n  }\n}\n```\n\n### External Provider Integration\nModular integration system in `SparkyFitnessServer/integrations/`:\n- **Food providers**: OpenFoodFacts, Nutritionix, FatSecret\n- **Exercise data**: Wger integration\n- **Health data**: Apple Health integration\n- **Encryption**: All API keys encrypted at rest using `security/encryption.js`\n\n### Frontend Context Providers\nKey contexts in `src/contexts/`:\n- **PreferencesContext**: User settings, goals, theme, AI provider config\n- **ChatbotVisibilityContext**: AI assistant state management\n\n### Database Schema Conventions\n- **UUID primary keys** for all tables (`gen_random_uuid()`)\n- **Audit fields**: `created_at`, `updated_at` on all tables\n- **RLS policies**: Users only access their own data\n- **Transaction patterns**: Multi-table operations wrapped in transactions\n\n## AI Integration Patterns\n\n### Multi-Provider AI Support\n```javascript\n// SparkyFitnessServer/ai/config.js\nfunction getDefaultModel(serviceType) {\n  switch (serviceType) {\n    case 'openai': return 'gpt-4o-mini';\n    case 'anthropic': return 'claude-3-5-sonnet-20241022';\n    case 'google': return 'gemini-pro';\n    // ...\n  }\n}\n```\n\n### Chat System Architecture\n- **Backend**: `routes/chatRoutes.js` handles AI provider routing\n- **Frontend**: `DraggableChatbotButton` component for UI\n- **Storage**: Chat history with metadata in PostgreSQL\n- **Capabilities**: Food logging, image analysis, exercise tracking, measurements\n\n## Data Flow Patterns\n\n### Meal Planning System\n1. **Templates** (`meal_plan_templates`) → **Scheduled Plans** (`meal_plans`) → **Food Diary Entries** (`food_diary`)\n2. Complex meal expansion logic in `mealPlanTemplateRepository.js`\n3. Date-range planning support with single-day granularity\n\n### Family Access Control\n- **Granular permissions**: `calorie`, `checkin`, `reports`, `food_list`\n- **RLS enforcement**: Database-level security policies\n- **Access patterns**: `familyAccessRepository.js` manages relationship permissions\n\n### External API Error Handling\nConsistent error patterns across integrations:\n```javascript\ntry {\n  const response = await fetch(searchUrl, { method: 'GET' });\n  if (!response.ok) {\n    const errorText = await response.text();\n    log('error', \"API error:\", errorText);\n    throw new Error(`API error: ${errorText}`);\n  }\n  return await response.json();\n} catch (error) {\n  log('error', `Error with query \"${query}\":`, error);\n  throw error;\n}\n```\n\n## Development Conventions\n\n### API Response Patterns\n- **Success**: JSON data directly\n- **Error**: `{ error: \"message\" }` with appropriate HTTP status\n- **Auth**: JWT tokens in Authorization header\n- **CORS**: Configured for frontend URL in environment\n\n### Database Migration Strategy\n- **Auto-migrations**: `utils/dbMigrations.js` runs on server startup\n- **Version tracking**: Migrations applied based on version numbers\n- **Schema changes**: Always backward compatible during development\n\n### Component Architecture\n- **shadcn/ui** for base components\n- **Custom components** in `src/components/`\n- **Page-level** components in `src/pages/`\n- **Shared utilities** in `src/utils/`\n\n### Environment-Specific Behavior\n- **Development**: Vite proxy, hot reload, debug logging\n- **Docker**: Production builds, environment variable injection\n- **Database**: Connection pooling, transaction management\n\n## Key Files for Understanding\n- **Server entry**: `SparkyFitnessServer/SparkyFitnessServer.js` (route registration, middleware setup)\n- **Frontend entry**: `src/App.tsx` (context providers, query client setup)\n- **Documentation site**: `docs/` - Comprehensive Nuxt Content documentation (https://codewithcj.github.io/SparkyFitness)\n\n## Documentation Structure\n\n### Developer Documentation\nThe developer documentation has been consolidated into focused guides at `docs/content/3.developer/`:\n\n1. **`1.getting-started.md`** - Complete setup, installation, and quick start guide\n2. **`2.architecture.md`** - System architecture, patterns, and project structure  \n3. **`3.contributing.md`** - Development workflow, guidelines, and PR process\n4. **`4.database.md`** - Database schema, migrations, and operations\n5. **`5.api-reference.md`** - OpenAPI specification and endpoint documentation\n6. **`6.troubleshooting.md`** - Comprehensive debugging guide and common solutions\n\n### Documentation Site Features\n- **Live site**: https://codewithcj.github.io/SparkyFitness hosted on GitHub Pages\n- **Technology**: Nuxt Content + Docus theme for optimal developer experience\n- **Navigation**: Auto-generated navigation with numerical ordering\n- **Search**: Full-text search across all documentation\n- **Mobile-friendly**: Responsive design for all devices\n","AGENTS.md":"# AGENTS.md\n\n*Last updated: 2026-10-22*\n\nThis is the repo-root monorepo guide for SparkyFitness. Use it to choose the right package, understand shared repo-level rules, and find the next guide to read.\n\n**For AI Tools & Developers:** Start with `agent-docs/README.md` (in this repo) for quick navigation to:\n- `file-and-domain-reference.md` — Find any code by feature in seconds\n- `testing-patterns.md` — Concrete test examples for each layer\n- `architecture-permissions.md` — Permission types and RLS patterns\n- Plus 5 more guides for migrations, data flow, anti-patterns, new features, and planning\n\nPackage-level guides win. For work inside a package, follow that package's `AGENTS.md` when present, otherwise its `CLAUDE.md`.\n\n## Scope\n\n- Start here when work begins at repo root or spans multiple packages.\n- Keep root-level guidance focused on workspace layout, shared conventions, and cross-package coordination.\n- Root `package.json` is tooling only (`husky`, `lint-staged`, `prettier`), not an app entrypoint.\n- Run scripts from the package directory you are changing.\n\n## Package Guides\n\n- Repo-root alias: `CLAUDE.md` points to this file.\n- Frontend: `SparkyFitnessFrontend/AGENTS.md`\n- Server: `SparkyFitnessServer/AGENTS.md`\n- Mobile: `SparkyFitnessMobile/AGENTS.md`\n- Shared: `shared/AGENTS.md`\n\nFor `docs/` and `SparkyFitnessGarmin/`, there is no package-level `AGENTS.md`. `SparkyFitnessGarmin/` is only a handful of Python files (`main.py`, `routes.py`, `service.py`, `schemas.py`); read them directly. For `docs/`, inspect the local manifest and content layout.\n\n## Monorepo Map\n\n- `SparkyFitnessFrontend/` - React 19 + Vite web app.\n- `SparkyFitnessServer/` - Express 5 + PostgreSQL backend API.\n- `SparkyFitnessMobile/` - Expo SDK 57 / React Native 0.86 app.\n- `shared/` - source-first TypeScript workspace package for `@workspace/shared` schemas, constants, and timezone/day helpers.\n- `docs/` - VitePress documentation site.\n- `docs/readme/` - translated copies of the root `README.md` (`README_<lang>.md`), outside the VitePress `srcDir`. The language bar at the top of `README.md` links to them; when the root README changes substantially, the translations go stale.\n- `SparkyFitnessGarmin/` - standalone Python integration service outside the current `pnpm` workspace.\n- `docker/`, `helm/`, `.github/` - infra and deployment assets.\n- `umbrel/sparkyfitness/` - Umbrel App Store package (manifest, compose, derived secrets), submitted to `getumbrel/umbrel-apps`. See `umbrel/README.md`.\n- `db_schema_backup.sql` - repo-root schema snapshot kept in sync by CI (`.github/workflows/schema-backup.yml`); never hand-edit or regenerate locally.\n- `docker/.env.example` - tracked env template commonly copied to repo-root `.env`.\n\n## Workspace Notes\n\n- `pnpm-workspace.yaml` currently lists `frontend`, `SparkyFitnessFrontend`, `shared`, `SparkyFitnessMobile`, `SparkyFitnessServer`, and `docs`.\n- Only `SparkyFitnessFrontend/` exists on disk right now; treat `frontend` as a legacy workspace entry unless the task is specifically about workspace cleanup.\n- `shared/` is a library package, not an app; its `pnpm run validate` runs typecheck, a hardcoded-string audit, and the Prettier check. CI runs that in a `Shared Validate` job and also re-runs the frontend, mobile, and server jobs whenever `shared/` changes, so validate shared changes from the consuming package(s), not in isolation.\n- `SparkyFitnessGarmin/` is outside the current workspace. Inspect its own manifest and scripts before working there.\n\n## Agent Efficiency (read this before searching)\n\nDo not read or search these paths; they burn context for nothing:\n\n- `WIP/` - personal scratch area; contains zips and full copies of other repos, including a stale duplicate of this repo (`WIP/SparkyFitness-main/`). Never read or edit anything under it.\n- `SparkyFitnessMobile/ios/` and `SparkyFitnessMobile/android/` - generated native projects (`ios/` is >1 GB of Pods). Regenerate with `npx expo prebuild --clean`; edit `app.config.ts`, `plugins/`, or `targets/` instead.\n- `pnpm-lock.yaml` (~1.3 MB) - never read; check `package.json` files instead.\n- `db_schema_backup.sql` (~330 KB) - never read whole; grep for the one `CREATE TABLE` you need.\n- `SparkyFitnessFrontend/dist/` - build output.\n- `SparkyFitnessFrontend/public/locales/` except `en/` - 35 machine-synced translations. Only `en/translation.json` is ever hand-edited, and even that (~120 KB) should be grepped, not read whole.\n\nCheap ways to learn things:\n\n- Database table index: read `docs/src/developer/database.md` (quick reference of all ~120 tables with one-line purpose). For detailed schema, read `shared/src/schemas/database/<Table>.zod.ts` (one small Zod file per table).\n- Database security & permissions: `docs/src/developer/database-security-tiers.md` (security tier, permission type, and RLS rules for every table).\n- API request/response contract: `shared/src/schemas/api/<Name>.api.zod.ts`.\n- Definition of done: CI (`.github/workflows/ci-tests.yml`) runs `pnpm run validate` (which includes Knip unused export & dead code checks in frontend and mobile) plus the package's CI test script for each changed package. Run those locally before declaring work complete.\n\n## Cross-Package Rules\n\n- If you add or change a server migration (such as creating a new table), follow `agent-docs/new-migration-checklist.md`. In short, you MUST:\n  1. Create the migration file in `SparkyFitnessServer/db/migrations/YYYYMMDDHHMMSS_description.sql`.\n  2. Update the Row-Level Security (RLS) policies in `SparkyFitnessServer/db/rls_policies.sql`.\n  3. **Restart the server** (`pnpm start` from `SparkyFitnessServer/`) to apply the migration.\n  4. Leave `db_schema_backup.sql` alone — after merge, CI regenerates it from the migrations and opens an automated sync PR (`.github/workflows/schema-backup.yml`). Never manually edit the backup file or commit a locally generated copy.\n  5. Add or update the matching Zod schema in `shared/src/schemas/database/`.\n  6. Update the user-facing documentation in `docs/src/features/family-friends-sharing.md`.\n  7. Update the developer documentation in `docs/src/developer/database-security-tiers.md` to classify the table as Tier 1, Tier 2, or Tier 3.\n- **Heart rate zones have one source of truth:** `shared/src/utils/heartRateZones.ts` (`resolveHeartRateZones`, `heartRateZoneRange`, `computeTimeInZones`). Never re-derive max heart rate or zone floors in a package; server stored zones, training load, mobile suggested ranges and the web/mobile charts and settings all call it. Max heart rate is the person's own value, else Nes (211 - 0.64 x age) from date of birth, else 190. Settings never rewrite stored `exercise_entry_hr_zones` rows. iOS 27 can fill custom floors from Apple Health (`floorsFromAppleHeartRateZones`); that does not change max heart rate. A finished Apple workout's own zone group (`bucketsFromAppleWorkoutZones`) is stored as `device` zones instead of recomputing, so the chart matches Apple, including time below the first boundary.\n- Prefer the shared timezone helpers from `@workspace/shared` and `SparkyFitnessServer/utils/timezoneLoader.ts` for day-string logic. Avoid `toISOString().split('T')[0]` for user-facing or business-logic dates.\n- Keep `YYYY-MM-DD` values as calendar-day strings until you reach a database or external API boundary that needs UTC instants.\n- Auth or API contract changes usually need a quick check in both web and mobile because they share the same backend.\n- Frontend local dev proxies `/api`, `/health-data`, and `/uploads` to the server on `3010`. The `/health-data` proxy is rewritten to `/api/health-data`, while server APIs remain rooted at `/api`.\n- Server runtime secrets are usually sourced from repo-root `.env`, commonly created from `docker/.env.example`. The server can also load secret files via `SparkyFitnessServer/utils/secretLoader.ts`.\n- Extract shared logic on the **second** duplication (\"rule of two\"), not the third - duplicated logic drifts as different sessions edit each copy. Extract *behavior*, not coincidental shape. See `agent-docs/anti-patterns.md`.\n- **Strict TypeScript Typing:** Never use `any` or `// eslint-disable-next-line @typescript-eslint/no-explicit-any` when creating new functions or editing existing code. When modifying existing files, proactively update legacy `any` declarations in the touched code to reflect their proper TypeScript data types, interfaces, or schemas from `@workspace/shared`. Do NOT copy legacy `any` parameter signatures forward when refactoring or extending service/repository files.\n- **Library Deletes vs Diary Snapshots:** `exercise_entries` and `food_entries` are self-contained snapshots, not pointers (`exercise_id` and `food_id` are `ON DELETE SET NULL`). Deleting an exercise or food from the library (`mode: 'delete'`) preserves past and current diary history, cascades from presets and plan templates, cleans up future scheduled workout plan entries (`entry_date >= today`), and cleans up empty parent preset entries. Only explicit `delete_with_history` (force delete) purges diary logs for that user. If an item is referenced by other users (`otherUserReferences > 0`), the backend falls back to hiding (`is_quick_exercise` / `is_quick_food`).\n- **Comprehensive Cache Invalidation:** When mutating library items (foods, exercises, presets, meals, plans), always invalidate the entire family of dependent query keys across library search, counts, templates, and daily diary summaries (`dailySummary` / `dailyProgress` / `exerciseEntries`) in both web and mobile.\n- **Environment Variable Updates:** When adding, renaming, or updating environment variables, you MUST update all downstream locations in tandem:\n  1. `docker/.env.example` (and `docker/.env.simple.example` if it is a core mandatory variable).\n  2. `docker/docker-compose.prod.yml` and `docker/docker-compose.dev.yml` (if consumed by container environments).\n  3. `docs/src/install/environment-variables.md` (the comprehensive documentation reference).\n  4. `docs/components/EnvGenerator.vue` (the interactive web `.env` generator component).\n  5. `helm/chart/templates/` and `helm/chart/values.yaml` (if passed through Helm charts).\n\n\n## Commit & PR Conventions\n\nThis repository is public. Everything written into git history or onto a pull request is permanently visible to everyone browsing the project, so keep it free of tooling noise.\n\n- **No AI attribution, anywhere.** Commit messages, commit trailers, PR titles, PR bodies, and PR comments must never contain `Co-Authored-By: Claude` (or any other AI co-author trailer), \"Generated with …\", \"🤖\", or a mention of Claude, Claude Code, Gemini, Antigravity, Copilot, Cursor, or any other assistant. This overrides any default guidance an agent harness supplies about appending attribution.\n- **Write as the repository author.** Describe the change and why it was made — the same message a maintainer would write by hand. Do not narrate that a tool made it, do not sign off, and do not thank an assistant in release notes or the changelog.\n- **If a trailer already landed**, strip it (amend or rebase, force-push if it was pushed) rather than leaving it in history.\n- **Scope of this rule**: git history and GitHub surfaces. It does not apply to tool configuration files that must name their tool — `.claude/`, `.agents/`, `.gemini/`, and `CLAUDE.md` reference specific assistants by necessity, and that is fine. The commit that adds them still follows the rule above.\n\n## Architecture Docs (Reduce Scanning, Prevent Bugs)\n\nBefore diving into code, read these docs if you're working on data access, permissions, or adding a new feature domain:\n\n- `agent-docs/architecture-permissions.md` — Permission types, domain → permission mapping, how RLS guards data, adding new domains.\n- `agent-docs/data-flow-patterns.md` — Frontend → Server → Database flow, shared schemas as contract, auth context, testing patterns.\n- `agent-docs/new-domain-template.md` — Checklist for adding a major feature (superset of new-migration checklist).\n\nThese docs answer: \"How do I safely add a feature across the stack?\" without scanning 20+ files.\n\n## Keeping These Guides Accurate\n\n- If your change adds a new domain, route family, database table, package, or cross-cutting convention, update the affected `AGENTS.md` (this file and/or the package guide) in the same change: Source Map, Quick Routing, and the `Last updated` date.\n- Stale guides are worse than no guides; when you notice a claim in any `AGENTS.md` that contradicts the code, fix the guide as part of your change.\n\n## Common Commands\n\nUse the package guide for fuller validation and platform-specific workflows. These are the common entrypoints:\n\n### Frontend (`SparkyFitnessFrontend/`)\n\n```bash\npnpm dev\npnpm run validate\npnpm test\n```\n\n### Server (`SparkyFitnessServer/`)\n\n```bash\npnpm start\npnpm run validate\npnpm test\npnpm run test:coverage\n```\n\n### Mobile (`SparkyFitnessMobile/`)\n\n```bash\npnpm start\npnpm run ios\npnpm run android\npnpm run validate\npnpm exec jest --watchman=false --runInBand\nnpx expo prebuild --clean\n```\n\n### Docs (`docs/`)\n\n```bash\npnpm dev\npnpm run build\n```\n\n","docs/CLAUDE.md":"# CLAUDE.md\n\nThis is the SparkyFitness documentation site built with **VitePress**. For work on docs, inspect `package.json` scripts and the `src/` layout directly (`.vitepress/config.mts`).\n","shared/AGENTS.md":"# AGENTS.md\n\n_Last updated: 2026-10-22_\n\n`@workspace/shared` is a source-first TypeScript workspace library package for schemas, constants, and timezone/day helpers consumed by SparkyFitnessServer, SparkyFitnessFrontend, and SparkyFitnessMobile.\n\n## Scope\n\n- This package defines contracts and shared logic, not an app.\n- Validate changes from consuming packages (server, frontend, mobile), not in isolation.\n- Every schema change here potentially touches three packages.\n\n## Food Assistant State\n\n`schemas/api/FoodAssistant.api.zod.ts` and `schemas/database/FoodAssistant.zod.ts` define lasting preferences, versioned tasks, unresolved ingredients and idempotent operation records. All state is owner-only; these additive contracts do not change existing chat clients.\n\n## Structure\n\n- `src/schemas/database/` - one Zod file per table (`Foods.zod.ts`, `Exercises.zod.ts`, ~115 files). Agent shortcut: to learn a table shape, read the matching file here instead of the SQL dump.\n- `src/schemas/api/` - API request/response contracts (`*api.zod.ts`). `VoiceTranscription.api.zod.ts` defines multipart limits/MIME types, transcription responses, voice preference patches, and compatible public service options shared by web/mobile/server.\n- `src/constants/` - shared constants and enums (exercises, nutrients, meal types, fasting protocols, medication schedules, cycle phases, etc.).\n- `src/utils/` - timezone helpers (`todayInZone`, `instantToDay`, `instantForDay`, `dayToUtcRange`, `compareDays`, `addDays`, `isDayString`), cycle/menstruation helpers, unit/calculation utilities, the race plan generator (`racePlan.ts`, `buildRacePlan`), the run program schedule helpers (`runProgramSchedule.ts`, `runProgramCatchUp.ts`, `runProgramTimeline.ts`: `buildProgramTimeline` / `itemsByDay`, and `runProgramActions.ts`: the `PUT /api/v2/run-program` body builders; web and mobile both use them), and the training pace zones (`racePaceZones.ts`: `racePaceZones` turns a recent race result into easy, interval, tempo, marathon-pace and stride ranges with Riegel's formula; `equivalent5kPace` is the shared 5K conversion; `paceZones.ts` holds the effort-based `paceZones`, `thresholdPaceFromEffort` and `PACE_ZONE_IDS`), and the plan realigner (`runProgramRealign.ts` (`realignProgram` rebuilds the rest of a plan to end on the goal day: drops repeats and the shortest easy runs first, folds lighter weeks, never touches the taper or final workout, keeps two runs a week for three-plus run days, stretches with repeated base weeks when there is room, eases after a long gap, and reports an unrealistic goal with the options; `applyRealign` is the confirmed change, `toRealignResult` the API shape); its contract is in `RunProgram.api.zod.ts`: `realignRunProgramBodySchema`, `runProgramRealignResultSchema`).\n- `src/utils/heartRateZones.ts` - the single source of truth for heart rate zones: `resolveMaxHr` (own value, else Nes from date of birth, else observed max, else 190), `resolveHeartRateZones` (three methods: `percent_max`, `heart_rate_reserve` (Karvonen, needs resting HR) and `custom` floors; falls back to percent of max when the chosen method lacks input and says so in `effectiveMethod`), `heartRateZoneRange(resolved, zone)` (bpm range of zone 1-5; the top zone ends at max HR - the function for training programs that need \"zone N in bpm\"), `computeTimeInZones` (buckets a heart rate series), `percentMaxFloors` / `heartRateReserveFloors`, `floorsFromAppleHeartRateZones` (five custom floors from Apple's preferred zones; null unless there are exactly five, and a missing zone-1 minimum becomes 30 bpm), `bucketsFromAppleWorkoutZones` (a finished workout's five zone durations, keeping time below the first boundary as zone 1; empty when Apple reported no time, null when the group is not five ascending zones). `trainingLoad.ts` weights sessions by these zones. The API contract is `src/schemas/api/HeartRateZones.api.zod.ts`.\n- `src/nutrients/` - nutrient catalogs and the goal calculator used by web onboarding, the web Goals editor and mobile Goals (`nutrientCalculation.ts`: per-nutrient recommended values from age, sex, weight, calories and the saved algorithm choices; `nutrientAlgorithms.ts`: the algorithm enums and labels).\n- `src/ai/`, `src/cycle/`, `src/medications/`, `src/mood/` - domain-specific helpers. `src/medications/adherence.ts` is the one implementation of medication adherence for web and mobile (`computeAdherence`, `computeChecklistProgress`, `entryMatchesDue`, and the month-calendar helpers `tallyEntriesByDay`, `dayLogStatus`, `tallySymptomsByDay`); `subtype.ts` holds the All | Meds | Supplements filter rules. Both are pure, take timezone and data as arguments, and are tested in `SparkyFitnessServer/tests/medicationAdherence.test.ts` and `medicationSubtype.test.ts`.\n- `src/types/` - shared plain TypeScript types that are not Zod-derived.\n- Routes: `src/schemas/api/Route.api.zod.ts` (points, turn cues, bodies), `src/utils/routeGeometry.ts` (distance, climb, simplify, bounds, and the `localProjector` / `distanceToSegment` nearest-point helpers the route editor uses), `src/utils/routeGpx.ts` (dependency-free GPX read/write), `src/utils/routeGenerate.ts` (generate and follow-roads request builders, error kinds, turn counts) and `src/constants/routingProviderTypes.ts` (the `valhalla` external provider type and its default public server URL).\n- Routes: `src/schemas/api/Route.api.zod.ts` (points, turn cues, bodies), `src/utils/routeGeometry.ts` (distance, climb, simplify, bounds) and `src/utils/routeGpx.ts` (dependency-free GPX read/write).\n- `src/compare/` - compare any two tracked things: `stats.ts` (statistics), `metrics.ts` (metric catalog: kind, missing-day rule, weekly form, and `rolling` for a value read over the 7 days ending on the day), `analyze.ts` (day and week-by-week analysis), `since.ts` (before vs. since a habit started: suggested start, stretches, regularity, pre-trend; its limits are exported as `SINCE_*` constants), `question.ts` (the question model web and mobile share: timing choices, spans and `compareWindow`, custom-range limits, since-start bounds, `SUGGESTED_QUESTIONS`, and the recents helpers `knownCompareQuestions` / `withRecentQuestion`), `chartData.ts` (chart data both apps draw: `pairedDots` and `displayDots`, `trendLine`, `overTimeData`, `sinceChartData`). The API contract is `src/schemas/api/Compare.api.zod.ts`.\n- `src/symptoms/` - generic symptom tracking: constants and enums (scales, templates, sections, option kinds), template section resolution (`resolveSections`), built-in symptoms and pick-lists, head/body region ids for the location maps, and custom-field validation. The API contract is `src/schemas/api/Symptoms.api.zod.ts`.\n\n## Naming Convention\n\n- `X.api.zod.ts` = API request/response schema\n- `X.zod.ts` = database table schema\n- Export everything from `src/index.ts`; consuming packages import both types and values via `@workspace/shared`\n\n## Cross-Package Contract Rules\n\n- Changes to `src/schemas/api/` usually affect server routes and both frontend/mobile API clients.\n- Changes to `src/schemas/database/` require matching server migrations and RLS policies when changing the stored schema. CI regenerates the schema backup after merge; never hand-edit or regenerate it locally. Mirroring existing columns missing from a Zod schema does not require another migration.\n- Timezone/day-string helpers prevent bugs; prefer them over `toISOString().split('T')[0]`.\n- Verified commands (run from `shared/`): `pnpm run typecheck`, `pnpm run format:check`, `pnpm run format`, `pnpm run i18n:audit`, `pnpm run validate` (typecheck + i18n audit + format check). The package has no lint or tests of its own; behavior is covered by the consumers' test suites.\n- Shared exports ids and data; apps own translation. `pnpm run i18n:audit` (`scripts/audit-user-facing-strings.mjs`) fails when a file gains more prose in `label`/`title`/`description`/`message`/`hint`/`summary`/`body`/`text` properties than `scripts/user-facing-strings.baseline.json` allows (the cycle, GLP-1, and body-region English catalogs are grandfathered). After moving text out, run it with `--update-baseline` to lower the allowance; use `// i18n-ignore` for literals that must stay.\n- Formatting uses the same `.prettierrc` as the other packages, and lint-staged formats staged `shared/` files on commit.\n- CI (`.github/workflows/ci-tests.yml`) runs `Shared Validate` and re-runs the frontend, mobile, and server jobs on any `shared/**` change.\n- Test any shared change from the consumer packages (`pnpm run validate` in SparkyFitnessServer, SparkyFitnessFrontend, and SparkyFitnessMobile after modifying shared).\n\n## Working Rules\n\n- Keep this package export-focused and schema-focused; logic that scales should live in consuming packages.\n- Never export stale or unfinished types; if a consumer is drafting code and needs a type not yet here, add it.\n","shared/CLAUDE.md":"# CLAUDE.md\n\nSee @AGENTS.md\n","SparkyFitnessFrontend/CLAUDE.md":"# CLAUDE.md\n\nSee @AGENTS.md\n","SparkyFitnessFrontend/AGENTS.md":"# AGENTS.md\n\n_Last updated: 2026-10-23_\n\nSparkyFitness Frontend is the React web app for the SparkyFitness monorepo. Use this file as the primary guide for work inside `SparkyFitnessFrontend/`.\n\nIf a task also touches the server, mobile app, or `shared/`, read that package guide before editing outside this directory. Use `../AGENTS.md` for monorepo-level context.\n\n## Scope\n\n- This file is for package-local work in `SparkyFitnessFrontend/`.\n- `CLAUDE.md` just imports this file via `See @AGENTS.md`.\n- Run scripts from this directory.\n\n## Current Snapshot\n\n- Stack: React 19, Vite 8, TypeScript 6, Tailwind CSS v4 (via `@tailwindcss/vite`), shadcn/ui-style Radix primitives, TanStack Query 5, React Router 7 (`createBrowserRouter`), i18next, Better Auth client, Zod 4, Recharts.\n- `@/*` maps to `src/`; `@workspace/shared` maps to `../shared/src/index.ts` (also in Jest via `moduleNameMapper`).\n- Dev server runs on port `8080` and proxies `/api`, `/mcp`, and `/uploads` to the backend on `3010`; `/health-data` is proxied with an `/api` prefix rewrite. Override the backend host with `VITE_BACKEND_HOST`.\n- PWA (`vite-plugin-pwa`) is enabled in production builds only.\n\n## Verified Commands\n\n```bash\npnpm dev\npnpm run typecheck\npnpm run lint\npnpm run format:check\npnpm run format\npnpm run validate\npnpm test\npnpm run test:ci\npnpm run build\n```\n\n- `pnpm run validate` runs typecheck, lint (`--max-warnings 0`), Prettier check, and Knip (`pnpm run knip` for unused files and exports) together.\n- `pnpm test` runs Jest (`ts-jest`, `jsdom`); config is inline in `package.json`, setup in `src/tests/setupTests.ts`.\n- `pnpm run build` runs `validate` first, then `vite build`.\n- CI (`.github/workflows/ci-tests.yml`) runs `pnpm run validate` and `pnpm run test:ci` for this package when its files change; matching those locally means a green PR.\n\n## Domain-Mirrored Layout (the most important convention)\n\nFeatures are organized by domain, and the same domain folder name appears in `src/pages/`, `src/api/`, and `src/hooks/`. A feature change usually touches the matching folder in all three:\n\n- Page domains: `Admin`, `Auth`, `Chat`, `CheckIn`, `Cycle`, `Diary`, `Errors`, `Exercises`, `Fasting`, `Foods`, `Goals`, `Integrations`, `Medications`, `Reports`, `Settings`, `Symptoms`.\n- API domains add a few more: `AiConversions`, `Chatbot`, `Onboarding`, `Pregnancy`, `SleepScience`, `Symptoms`.\n- Example: a Medications bug lives in `src/pages/Medications/` + `src/api/Medications/` + `src/hooks/` medication hooks. Start there, not with a repo-wide search.\n\n## Source Map\n\n- Food assistant state: owner-only preferences, recoverable tasks and operation history use `FoodAssistant.api.zod.ts` / `FoodAssistant.zod.ts` in shared. Server: `routes/v2/foodAssistantRoutes.ts`, `services/foodAssistantService.ts`, `models/foodAssistantRepository.ts`, `ai/tools/foodAssistantTools.ts`. Web: `src/components/ai/FoodAssistantState.tsx`, `src/api/Chatbot/foodAssistantService.ts`.\n\n- `src/main.tsx` - app bootstrap; creates the shared `QueryClient` with global `QueryCache`/`MutationCache` handlers that render toasts from query/mutation `meta` (`errorTitle`, `errorMessage`, `successMessage`).\n- `src/App.tsx` - route registry via `createBrowserRouter`, plus `PrivateRoute` and `PermissionRoute` wrappers (permission-gated areas include `reports` and `admin`).\n- `src/pages/<Domain>/` - route screens by domain.\n- `src/pages/Reports/Compare/` - the Reports > Compare tab (`CompareReport` is the root, mounted by `Reports.tsx` outside its loading gate), with its data from `src/hooks/Reports/useCompare.ts` and `src/api/Reports/compareApi.ts`.\n- `src/api/api.ts` - `apiCall(endpoint, options)` helper: base URL `/api`, query `params`, JSON/FormData bodies, `responseType`, error toasts, `suppress404Toast`. Use it for all backend requests.\n- `src/api/<Domain>/` - per-domain API clients built on `apiCall`.\n- `src/hooks/<Domain>/` and `src/hooks/use*.ts(x)` - TanStack Query hooks and shared UI hooks (`use-toast`, `useDebounce`, `useAuth`, ...).\n- `src/components/` - shared components; `ui/` holds the shadcn-style primitives (~37 files); domain component folders include `Foods/`, `FoodSearch/`, `FoodUnitSelector/`, `Onboarding/`, `ExerciseCharts/`, `ai/` (assistant-ui chat pieces).\n- `src/contexts/` - `ActiveUserContext` (family-access acting-user switching), `PreferencesContext`, `ThemeContext`, `WaterContainerContext`, `ChatbotVisibilityContext`, `ChatToolCategoriesContext` (runtime chat tool-category selection, localStorage-backed).\n- `src/layouts/` - `MainLayout.tsx` and `AddComp.tsx`.\n- `src/lib/` - `auth-client.ts` (Better Auth React client), `utils.ts` (`cn`), scanner engines, sleep helpers.\n- `src/services/` - pure calculation helpers (BMR, body composition, nutrient calculation), not HTTP clients.\n- `src/utils/` - logging, user preferences, date helpers, misc. `browserVoiceInput.ts` owns browser speech/recording lifecycle; `hooks/AI/useVoiceInput.ts` appends transcripts to drafts without sending.\n- `src/tests/` - Jest suites mirroring `components`/`contexts`/`hooks`/`services`/`utils`, plus `test-utils.tsx`.\n- `public/locales/<lng>/translation.json` - i18next resources, loaded over HTTP at runtime.\n\nWhen searching, ignore `node_modules/`, `dist/`, and every locale except `public/locales/en/`.\n\n## Translations (i18n)\n\n- Only ever edit `public/locales/en/translation.json`. The other 35 locales are machine-synced through the `sync-translations.yml` workflow and a separate SparkyFitnessTranslations repo; hand-editing them creates conflicts with that pipeline.\n- UI strings go through `useTranslation()` / `t('...')` keys, not hardcoded literals.\n- `en/translation.json` is ~120 KB - grep for the key or section you need instead of reading the whole file.\n- Developer docs: `../docs/src/developer/translations.md`.\n\n## Conventions\n\n- Use `apiCall` (or an existing per-domain client) for backend requests; don't hand-roll `fetch`.\n- Prefer declaring toast text via React Query `meta` on the query/mutation instead of imperative `toast(...)` calls where the global handlers cover it.\n- Use `src/utils/logging.ts` helpers instead of bare `console.*`; verbosity follows the user's logging-level preference.\n- Keep `YYYY-MM-DD` values as calendar-day strings; use the shared timezone/day helpers from `@workspace/shared` instead of `toISOString().split('T')[0]`.\n- To learn a database table's shape, read `../shared/src/schemas/database/<Table>.zod.ts` - do not read `../db_schema_backup.sql` or the migrations.\n- Auth flows go through `src/lib/auth-client.ts` and `useAuth`; acting-user (family access) state lives in `ActiveUserContext` and affects most data hooks.\n- New UI should reuse `src/components/ui/` primitives and existing shared components before adding new ones.\n- Food and meal quantity fields use `NumericInput` with `allowExpressions` for basic arithmetic. `utils/evaluateArithmetic.ts` parses numbers, parentheses, and `+ - * /` without executing code; invalid expressions block form submission. Other numeric fields retain their existing behavior.\n- Time entry uses `TimeInput` (`'HH:mm'`) and `DateTimeInput` (`'YYYY-MM-DDTHH:mm'`) from `src/components/ui/TimeInput.tsx`, never native `<input type=\"time\">` or `type=\"datetime-local\"`: browsers pick 12h/24h from their language and ignore the user's `timeFormat` preference. Both take the native string formats, so call sites keep their state as-is; name them with `aria-labelledby` (an external `<Label htmlFor>` cannot target a multi-part field).\n- **Cache Invalidation on Library Mutations:** When mutating foods, exercises, presets, or plans, use the domain invalidation hooks from `src/hooks/useInvalidateKeys.ts` (`useExerciseInvalidation`, `useFoodInvalidation`, `useMealInvalidation`, `useDiaryInvalidation`) to invalidate the entire family of dependent query keys including search, presets, templates, and diary daily progress.\n- **Library Deletes & Snapshots:** Deleting foods or exercises uses `mode: 'delete'` which preserves logged diary history (via snapshots) and drops items from presets/plans; only explicit `delete_with_history` deletes diary entries. Empty presets are guarded against starting/logging.\n\n## Testing and Validation\n\n- Test files live in `src/tests/`, mirroring the source area they cover; use `test-utils.tsx` for rendering with providers.\n- Run the tests nearest the touched surface first, then `pnpm run validate` for cross-cutting changes.\n- Lint is strict (`--max-warnings 0`); unused imports fail the build.\n\n## Quick Routing\n\n- Routing/navigation/permission issue: `src/App.tsx` (router, `PrivateRoute`, `PermissionRoute`) and `src/layouts/MainLayout.tsx`.\n- API/error-toast issue: `src/api/api.ts`, then the domain client in `src/api/<Domain>/`, then the query/mutation `meta` in the calling hook.\n- Auth/session issue: `src/lib/auth-client.ts`, `src/hooks/useAuth.tsx`, `src/pages/Auth/`, and the server's `auth.ts` if it crosses packages.\n- Family-access/acting-user issue: `src/contexts/ActiveUserContext.tsx` and the hooks consuming it.\n- Chat (Sparky) issue: `src/pages/Chat/`, `src/components/ai/`, `src/api/Chatbot/`. Voice controls live in `VoiceInputControl.tsx`; `VoiceInputSettings.tsx` shares server-backed enable/System/AI choices with mobile. `active_voice_ai_service_id: null` means System, never automatic inheritance from Chat. A compatible Chat model is only a suggestion. Cancelling, leaving the page, hiding the tab, or changing voice preferences releases the microphone and aborts uploads.\n- Theme/preferences issue: `src/contexts/ThemeContext.tsx`, `src/contexts/PreferencesContext.tsx`, `src/api/Settings/preferences.ts`, `src/utils/userPreferences.ts`.\n- Calorie goal settings: `src/pages/Settings/CalculationSettings.tsx`; `components/WeeklyWeightGoalInput.tsx` edits the `weekly` goal mode with direction and kg, g, lbs or stones + pounds per week, defaulting to the profile weight unit. Store signed kilograms in `goal_mode_weekly_change_kg` (negative loss, positive gain, zero maintenance), preserving legacy percentage modes. Use shared `formatWeeklyGoalRate` / `parseWeeklyGoalRate` for display and input; switching units preserves the canonical rate. Pass `weeklyChangeKg` to `computeCalorieTarget`; Diary preview and `CalorieTargetBreakdown` use the same preference.\n- Missing/wrong UI text: the i18n key in `public/locales/en/translation.json` and the `t('...')` call site.\n- Exercise alternatives / workout feedback / adaptive suggestions (#1560): `AddExerciseDialog`'s `replaceFor` prop adds the **Suggested** tab (`pages/Exercises/ExerciseAlternativesPanel.tsx`); the workout player's Replace (lazy-loaded dialog) and load pass (`WorkoutPlaybackPage.tsx`, helpers in `utils/workoutPlayback.ts`) apply the shared `decideAdaptiveAdjustment` rules and keep both the usual and adapted sets on the draft for \"Use my usual\"; feedback is `pages/Diary/WorkoutFeedbackPanel.tsx` (finish dialog + expanded diary workout) via `hooks/Exercises/useWorkoutCoaching.ts`.\n- Medication adherence, streak, checklist progress and month-calendar status (`pages/Medications/TodayMedications.tsx`, `MedicationLogCalendar.tsx`) are computed by `@workspace/shared` (`medications/adherence.ts`), the same code the mobile adherence screen uses; change the rules there, not in the page. The meds/supplements filter rules (`filterMedsBySubtype`, `filterEntriesBySubtype`, `visibleDoseCards`) come from `medications/subtype.ts`.\n- Symptom or episode tracking (#1882): `src/pages/Symptoms/` (`SymptomsHub` is the tracker, shown on Check-in through `SymptomsPanel` and on Medications with `variant=\"medications\"`; `SymptomLogForm` is template-driven, `SymptomsReport` is Reports > Symptoms), `src/api/Symptoms/symptomService.ts`, `src/hooks/useSymptoms.ts`. Which sections a symptom asks about comes from `resolveSections` in `@workspace/shared`; family delegates with only the symptoms permission use the standalone `/symptoms` page.\n- Bodyweight exercises (#56): the `bodyweight_reps` layout in `constants/exercises.ts` (`signedWeight`) drives the +/− weight header and placeholder in the set editor; reports and trend charts count body weight through the shared `effectiveLoadKg` / `setVolumeKg` (`pages/Reports/ReportsTables.tsx`, `utils/exerciseTrendUtils.ts`, which reads `body_weight_kg` from the progress endpoint).\n- `weight_distance` / `weight_duration` (#127) have their own `SET_TABLE_LAYOUT` entries: carries swap the reps cell for a distance cell (stored km, edited in metres/yards via `carryDistanceFromKm` / `carryDistanceToKm` in `@workspace/shared`), loaded holds drop reps and use the duration column.\n- Training consistency (#59): `pages/Reports/TrainingConsistencyCard.tsx` in the Exercise report's left column, fed by `useTrainingConsistency` (`hooks/Reports/useReports.ts`) from `GET /reports/training-consistency`. A fixed 26-week window computed on the server by the shared `buildTrainingConsistency`, so it ignores the report's date filter; the existing `WorkoutHeatmap` stays the calendar.\n- Compare tab (day and week questions): `src/pages/Reports/Compare/CompareReport.tsx` holds the question and span state and wires the sentence, charts and tables; wording is `compareCopy.ts` and table rows are `compareTables.ts`; question, window and display-scale logic is `shared/src/compare/`. Since-habit questions are not offered on the web yet. It shows only the signed-in user's own data (no request while acting on behalf), and its range is its own, so the Reports date picker is hidden on this tab.\n- Shoe inventory: the Equipment section of the Exercises page (`src/pages/Exercises/EquipmentManager.tsx` + `ShoeFormDialog`; there is no separate tab or route), `src/components/Shoes/ShoeSelect.tsx` (the picker used by `pages/Diary/EditExerciseEntryDialog.tsx` for cardio entries), `src/api/Shoes/shoeService.ts`, `src/hooks/Shoes/useShoes.ts`. Distances are stored in km and edited in the user's unit; assigning a shoe invalidates both `shoeKeys` and `exerciseEntryKeys` because mileage is derived from linked entries. The Equipment section also shows while acting on behalf of someone: `can_manage_diary` delegates can edit it and `can_view_reports` delegates read it, as the server enforces.\n- Training programs (run and walk plans): the `/exercises/training-programs` route (`src/pages/Exercises/TrainingPrograms/`: `TrainingPrograms` is the page, `TrainingProgramPicker` the built-in list with the 3 or 4 runs a week choice and the person's own plans, `StartProgramDialog` the start day, `TrainingProgramCard` the running program, `TrainingProgramCalendar` the month view, projected finish day, goal-date fit and catch-up), reached from `TrainingProgramsEntry` on the Exercises page, and `pages/Diary/RunProgramBanner.tsx` in the Diary's exercise card. Data is `GET/PUT /api/v2/run-program` plus `POST .../complete` and `GET .../custom` through `src/api/Exercises/runPrograms.ts` and `src/hooks/Exercises/useRunProgram.ts`; the server route is owner-only (`requireSelfActor`), so every hook and the banner stay off while `isActingOnBehalf`. All schedule maths comes from `@workspace/shared` (`projectRuns`, `projectedFinishDay`, `programRunForDate`, `catchUpFor`, `buildProgramTimeline`), never re-derived here. Request bodies come from `utils/runPrograms.ts`; `useSaveRunProgramMutation` runs changes one at a time (mutation `scope`) and accepts a function of the program as it stands, so two quick weekday taps both land. Mutations write the response into `runProgramKeys.current()` and call `useRunProgramInvalidation` (program, workout plan, exercise entry and daily progress families); `useDiaryInvalidation` and `useExerciseInvalidation` also refresh `runProgramKeys.all`. English program names, descriptions and week layouts live in `constants/runProgramCopy.ts` and mirror `trainingPrograms.programs.<id>.*` in the locale file. The web does not record runs: **Mark workout as done** (card and Diary) is how a workout is ticked off. Not on web yet: creating or editing custom plans, the race plan builder, the effort rating, the ease-back offers and the workout detail screen.\n- Heart rate zones: Settings → Profile & Account → Heart Rate Zones (`src/pages/Settings/HeartRateZoneSettings.tsx`, `src/hooks/Settings/useHeartRateZones.ts`, `src/api/Settings/heartRateZones.ts` over `GET`/`PUT /api/v2/heart-rate-zones`). Zone maths is shared (`@workspace/shared` `heartRateZones.ts`); labels are in `src/utils/heartRateZoneLabels.ts`. `ActivityHeartRateZonesChart` takes `zoneMethod` (from the stored `zone_method` of the workout's zone rows) and says which method the zones were worked out with; stored zones are never recomputed when settings change.\n- Routing provider (Valhalla, route generation and Follow roads): a Settings > External Providers type like Mealie, with `pages/Settings/RoutingProviderFields.tsx` (empty server URL with an example placeholder and no default server, optional API key, privacy and public-server notes) used by `ProviderSpecificFields` (add) and `EditProviderForm`; the type row is seeded by the server migration, the server resolves it per person (see the server guide).\n- Chart issue: Recharts usage in the domain page plus `src/components/ExerciseCharts/` or `ZoomableChart.tsx`.\n\n## Priority Rule\n\n- For work inside `SparkyFitnessFrontend/`, this file wins over repo-root guidance on package-specific details.\n- If a task spans packages, combine this guide with the other affected package guides.\n- If you add a new domain folder, route family, or cross-cutting convention, update the Domain list, Source Map, and Quick Routing sections of this file in the same change.\n","SparkyFitnessMobile/AGENTS.md":"# AGENTS.md\n\n_Last updated: 2026-10-23_\n\nSparkyFitness Mobile is a React Native 0.86 + Expo SDK 57 app for syncing Apple Health / Health Connect data with the SparkyFitness backend, tracking nutrition, hydration, fasting, measurements, exercise, saved foods, meal templates, custom exercises, workout presets, iOS / Android widgets, the active workout HUD, and the Sparky AI chat.\n\nThis is the package guide for `SparkyFitnessMobile/`. Work from this directory for mobile implementation and validation. If a task crosses into the backend, frontend, or `shared/`, read that package guide too before editing outside mobile.\n\n## Scope And Style\n\n- TypeScript is strict. Keep changes type-safe and compiling cleanly.\n- Prefer small, direct changes that fit the existing screen, hook, and service boundaries.\n- For ambiguous bugs, prove which layer is failing before patching. One narrow diagnostic check beats speculative edits across multiple layers.\n- Do not replace a working implementation with a rewrite unless the requester explicitly approves that direction.\n- When asked to plan work, confirm scope with clarifying questions before exploring code or drafting the plan.\n- Run scripts from `SparkyFitnessMobile/`, except root package operations such as `pnpm install` for patched dependencies.\n- Treat `android/` and `ios/` as generated output when possible. Edit `app.config.ts`, `plugins/`, `targets/`, JS/TS sources, or patch files first, then regenerate with prebuild when needed.\n\n## Stack And Imports\n\n- Primary stack: React 19.2, React Native 0.86, Expo SDK 57, TypeScript 6, React Navigation 7, TanStack Query 5, Uniwind / TailwindCSS v4, Reanimated 4, Skia, Victory Native, Expo Background Task / Task Manager / Notifications, Zustand, assistant-ui + AI SDK (chat).\n- `@/*` maps to this package and `@workspace/shared` maps to `../shared/src/index.ts`.\n- Prefer `@workspace/shared` schemas, constants, date/timezone helpers, and types over local duplicates.\n- The app talks to the backend under `/api`; health uploads go to `POST /api/health-data`.\n- Global `fetch` is Expo's WinterCG `expo/fetch`, so React Native's `{uri, name, type}` FormData file parts throw \"Unsupported FormDataPart implementation\". Append an `expo-file-system` `File` (it implements Blob) for multipart uploads; see `pregnancyPhotosApi.ts`.\n- Server-stored distance/weight units are metric. UI conversion belongs in mobile helpers such as `unitConversions.ts`.\n\n## Localization contract\n\nEnglish (`en`) is the canonical source locale and the deterministic fallback. Feature developers must add/update the English catalog, use semantic static keys, provide explicit English fallback/defaultValue text, use count-based i18next pluralization, use app-locale date/number/unit formatters, and avoid user-facing hardcoded text. Custom, user, and server content stays literal.\n\nFeature developers do not need to know or translate Polish or any future language, and do not need to wait for Weblate. Translators/Weblate own Polish and future translations and linguistic QA. Missing translation content is non-blocking and falls back to English; existing translated content remains structurally validated.\n\nThe shipped locale registry is `src/localization/localeRegistry.json`, read through the typed accessors in `src/localization/localeRegistry.ts`. Adding a catalog to Weblate does not ship it. Shipping requires explicit registry enablement plus native/platform support verification. RN catalogs and native resources are separate surfaces (Expo metadata, Android widget resources, and iOS widget/Live Activity `.lproj` resources), each its own Weblate component and each synced by `.github/workflows/sync-translations.yml`. Only the `en` side of any surface is edited by hand; the i18n audit blocks on registered locales and reports unregistered ones without failing. The workflow pulls the widget resources for registered locales only, because Android and iOS compile whatever resource directories are present and there is no build-time registry check for them.\n\n## Commands\n\n```bash\npnpm start\npnpm run ios\npnpm run android\npnpm run lint\npnpm run typecheck\npnpm run validate\npnpm exec jest --watchman=false --runInBand\npnpm exec jest --watchman=false --runInBand <test-path>\npnpm run test:coverage -- --watchman=false --runInBand\nnpx expo prebuild --clean\n```\n\n- `pnpm run validate` runs i18n and muscle-figure generation checks, typecheck, lint, i18n audit, watch i18n audit, Knip (`pnpm run knip` for unused files and exports), native locales check, and Prettier check together.\n- Use Watchman-disabled Jest commands in agent/sandbox runs; bare Jest often fails on macOS.\n- `collectCoverage` is enabled in Jest config, so expect coverage output from normal test runs.\n- Run `npx expo prebuild --clean` after native dependency changes, permissions, app group or widget target changes, Expo plugin changes, native config edits, or patching native modules.\n- After editing the root `patches/react-native-health-connect@4.1.3.patch`, run `pnpm install` from the repo root, then prebuild from mobile.\n\n## App Shell And Navigation\n\n- `App.tsx` is the root composition point. `App()` wraps `QueryClientProvider`, `KeyboardProvider`, `GestureHandlerRootView`, and `BottomSheetModalProvider`; `AppContent()` owns `NavigationContainer`, `SafeAreaProvider`, navigators, `AddSheet`, auth modals, the embedded/floating active-workout bars, the tab-bar `WhatsNewBanner`, and toasts.\n- App-shell logic lives in dedicated hooks that `AppContent` composes: `useAppBootstrap` (`src/hooks/useAppBootstrap.ts`) owns language initialization, initial-route selection, the initial linking-enabled state, and splash hiding; `useAppStartup` (`src/hooks/useAppStartup.ts`) owns the one-time startup services (theme, notifications, notification actions, background sync, HealthKit observers); `useAutoSyncOnOpen` (`src/hooks/useAutoSyncOnOpen.ts`) for cold-start/foreground-return sync and the observer-yield window; `useAddSheetActions` (`src/hooks/useAddSheetActions.ts`) for AddSheet handlers and last-active-tab tracking. Error-boundary-wrapped `Safe*` screen components live in `src/navigation/safeScreens.tsx`.\n- Startup initializes theme, haptics, sounds, notification prefs, logs, timezone bootstrap, background sync, pending cache refreshes, fasting/hydration card visibility, and platform health observers.\n- Initial route comes from `getActiveServerConfig()` inside `useAppBootstrap`: no active config lands on `Onboarding`; otherwise users enter `Tabs`. A language-initialization failure is logged and never changes the route.\n- Deep links are enabled only after startup confirms `Tabs`, so widget links do not bypass first-run onboarding.\n- `CompareReport` is a stack route (`SafeCompareReport`, title `screens.compareReport`) opened from the Reports hub.\n- Navigation source of truth is `App.tsx` plus `src/types/navigation.ts`; update both and the linking config when routes change.\n- Root stack uses `@react-navigation/native-stack`. Tabs use `@react-navigation/bottom-tabs`.\n- Tabs are `Dashboard`, `Diary`, `Add`, `Library`, and `Settings`. `Add` is a center action in `CustomTabBar`, not a content screen.\n- Native iOS Liquid Glass tabs use `@bottom-tabs/react-navigation` in `src/components/TabsLayout.tsx`; each content tab is wrapped in its own `createNativeStackNavigator` so the tab path still gets native headers. The `Add` tab uses `role: 'search'`, which the iOS 26 SDK draws as the detached round button. The iOS 27 SDK no longer does, so `patches/react-native-bottom-tabs@1.4.0.patch` (a pnpm patch) leaves the role off on iOS 27+ and sets the tab controller's `prominentTabIdentifier` (the iOS 27 API for a separated trailing tab) to that tab instead. Drop the patch once the library does this itself.\n- `TabsLayout` switches at runtime via `useNativeIOSTabsActive()` (`services/nativeTabBarPreference.ts`): native tabs require iOS 26+ Liquid Glass support AND the user's `liquidGlassTabBarEnabled` preference (opt-in). Everything else renders `CustomTabBar` (`TAB_BAR_HEIGHT = 56`); `ActiveWorkoutBar` reads the native tab-bar height instead when native tabs are active.\n- When adding a root-stack screen, add the route to `RootStackParamList` and register a matching `<Stack.Screen>` in `App.tsx` with `createStackScreenOptions(...)` or equivalent explicit iOS native-stack header options.\n- Native header option/button builders live in `utils/nativeHeaderItems.ts` (`createIOSNativeHeaderOptions`, `createIOSSmallNativeHeaderOptions`, text/icon button items); header action colors come from `useHeaderActionColors`, which is Liquid-Glass-aware on iOS.\n- Root-stack screens with a screen-owned React header are automatically checked by `__tests__/navigation/nativeHeaderContract.test.ts`; do not add screen-specific native-header allowlists.\n- Root-stack screens with a screen-owned React header and a real back button must set `headerBackTitle` or `headerBackButtonDisplayMode: 'minimal'` in `App.tsx` so iOS back labels stay explicit or intentionally hidden; close/cancel modal headers do not need either option.\n- Declare screen headers with `useScreenHeader(config)` (`src/hooks/useScreenHeader.tsx`). One declarative descriptor (`title`/`nativeTitle`, `left`/`right` items of kind `back`/`dismiss`/`text`/`icon`/`primary`/`menu`, `busy`/`disabled`, `animateKey` for view↔edit cross-fades) renders both paths: on the native path it mirrors items into `unstable_header{Left,Right}Items` via a layout effect and returns `null`; on the custom path it returns the bar element for the screen to render. A `menu` item is a declarative dropdown (plain actions and/or titled single-select sections, optional accent-dot `showsBadge`): the native path builds a system UIMenu header button, the custom path renders the trigger plus an `AnchoredMenu` under it — one item list drives both, as in `FoodsLibraryScreen`'s ownership filter. Hook screens must not keep hand-rolled header code (no `unstable_header*Items` blocks or custom bars alongside the hook) — the contract test enforces this.\n- Path selection is `useNativeIOSHeadersActive()` (`services/nativeTabBarPreference.ts`): always false on Android; on iOS it is true below iOS 26 (classic native headers) and follows the Liquid Glass toggle on iOS 26+, so turning the toggle off swaps in the same screen-owned fallback headers Android renders.\n- One-accent rule: exactly one primary header action per screen (`kind: 'primary'` or `role: 'primary'`), enforced with a `__DEV__` throw; primary header save buttons fall back to the localized `t('common.save')` / `t('common.saving')` labels when no `label`/`busyLabel` is supplied. Footer-save forms mark their header Save `placement: 'native-only'` so the custom bar does not duplicate the sticky-footer button. `onPress` handlers dispatch through the hook's internal ref map — do not add per-screen handler-ref effects for native header buttons.\n- A **right-slot** `kind: 'primary'` press is wrapped in the shared synchronous duplicate-press guard (`utils/duplicatePress.ts`, same one `FooterSaveBar` uses): presses inside `DUPLICATE_PRESS_WINDOW_MS` collapse to one. `disabled`/`busy` are React state and have not committed when taps queued behind a blocked JS thread replay, so every queued press otherwise ran the handler again (#2191). The guard is deliberately time-based, not a latch on `busy`, because handlers that never report a pending state would leave the button dead. **Left-slot** primaries are exempt — that slot is navigation, and `CycleOnboardingScreen` uses the sugar for a wizard Back where repeated presses are intended. A screen that needs a rapidly repeatable right-slot action must not use `kind: 'primary'`.\n- If a root-stack screen is intentionally presented above `Tabs` instead of inside native-tabs mode, document it in `NATIVE_TABS_ROUTE_EXCLUSIONS` in `__tests__/navigation/nativeHeaderContract.test.ts` with a short reason.\n- Screens intentionally off the hook (e.g. `FoodSearchScreen`'s bespoke anchored-menu bar) must mirror custom actions with `unstable_header{Left,Right}Items` themselves, hide the screen-owned React header behind `useNativeIOSHeadersActive()` with a guard such as `{!usesNativeHeader && <Header />}`, and gate the `useLayoutEffect` that sets native header items on the same flag; otherwise iOS renders both headers.\n- When adding a tab, update `TabParamList`, `NativeTab.Screen`, and `FallbackTab.Screen`; for content tabs also add a tab-local native stack screen using `createIOSNativeHeaderOptions(...)`.\n- `__tests__/navigation/nativeHeaderContract.test.ts` enforces this native-header wiring. If it fails, fix the route/type/navigator alignment instead of weakening the test.\n- Current stack screens include onboarding/tabs, library/detail/form flows for foods/meals/meal plans/exercises/presets, food entry view/edit, meal type detail and copy, the family diary flows (`FamilyMembers`, `FamilyDiary`, `FamilyMealDetail`, and `FamilyCopyReview`), `EditBarcode`, food search/entry/scan/photo flow, workout/activity add/detail, exercise/preset search, settings subscreens, logs, sync, measurements, the progress photo flow (`ProgressPhotos`, `ProgressPhotoCompare`, and `ProgressPhotoTimelapse`), `ExerciseStatistics` and `CardioSession` (with `ActivityShare`, opened from its Share as image button: a 4:5 `ActivityShareCard` of stats and the GPS route over a picked photo or a transparent background, captured with Skia `makeImageFromView` in `services/activityShareImage.ts` and shared as a PNG; color from `utils/shareColor.ts`), `Shoes` and `ShoeForm`, fasting, mindfulness (`MindfulnessDetail` and `MindfulnessLog`), and `WhatsNew`.\n- `AddSheet` offers Food, Workout, Activity, Preset, Measurements, Scan Food, Progress Photos, Mood, Mindfulness, Ask Sparky, and Sync Health Data. Its Mood row opens `MoodLog`, and the Dashboard has a toggleable `mood` card (today's mood, 7-day trend) that opens it (mood faces, tags, note, and a mood-over-time chart; one entry per day via `POST /api/mood`). Its Progress Photos row opens `ProgressPhotos` on the diary's active date. Its Mindfulness row opens `MindfulnessLog` (`MindfulnessLogScreen`), a root-stack modal laid out like `MoodLog` and `MeasurementsAdd` (screen header, scrolling form, sticky `FooterSaveBar`, so the navigator owns Android's Back); `MindfulnessDetail` opens the same screen from the Log Mindful Session row on its Practice & Log tab and, with a `session` param, from Edit on its History tab, so it has no log form of its own. Session type names and icons come from `utils/mindfulnessSessionTypes.ts`, shared by the log screen, the detail screen and `MindfulnessCard`; the list is the shared `mindfulnessSessionTypeSchema`, and a stored type it does not have shows as Other and is kept on save. Paced Breathing and the Meditation Timer keep Finish & Save on screen while the save runs, so they go through `useSaveOncePerRun`: a run is saved once (presses are dropped until the practice is reset, taps replayed before that reset renders included), a failed save can be retried, and a save from a run closed while pending does not reset the next one. `MindfulnessLog` goes back after a save only while it is still focused, because it can be opened from a screen other than the tab host. Keep `AddSheet`'s present/dismiss refs intact to avoid Android re-present loops. Android's hardware Back in `AddSheet` does what the sheet's own Back link does: from the Exercise menu it returns to the main menu, from the main menu it closes the sheet.\n- Current stack screens include onboarding/tabs, library/detail/form flows for foods/meals/meal plans/exercises/presets, food entry view/edit, meal type detail and copy, the family diary flows (`FamilyMembers`, `FamilyDiary`, `FamilyMealDetail`, and `FamilyCopyReview`), `EditBarcode`, food search/entry/scan/photo flow, workout/activity add/detail, exercise/preset search, settings subscreens, logs, sync, measurements, the progress photo flow (`ProgressPhotos`, `ProgressPhotoCompare`, and `ProgressPhotoTimelapse`), `ExerciseStatistics` and `CardioSession`, `Shoes` and `ShoeForm`, fasting, mindfulness (`MindfulnessDetail` and `MindfulnessLog`), and `WhatsNew`.\n- `AddSheet` offers Food, Workout, Activity, Preset, Measurements, Scan Food, Progress Photos, Mood, Mindfulness, Ask Sparky, and Sync Health Data. Its Mood row opens `MoodLog`, and the Dashboard has a toggleable `mood` card (today's mood, 7-day trend) that opens it (mood faces, tags, a Journal box, and a mood-over-time chart). `useMoodCheckinsSupport` probes `GET /api/mood/date/<today>` with `X-Mood-Model-Version: 2` the first time the `mood` card, `MoodLog` or `MoodDay` mounts, and the answer is cached for the session: an array back means the server takes several check-ins a day and any other answer keeps the one-entry-per-day screen via `POST /api/mood`. A failed probe with nothing known shows a Retry on `MoodLog` and `MoodDay` (`components/mood/MoodSupportCheckFailed`) rather than either form, because the one-entry POST on a v2 server edits the day's latest check-in instead of adding one. On a v2 server the `mood` card shows one mood per day (the average of your own moods, else the Garmin or imported one), its body always opens `MoodDay` (the day's check-ins in time order with the day's averages; tap to edit in `MoodLog`, swipe or long-press to delete, Add check-in for a new one), which replaces itself with `MoodLog` for the day on an older server, and its \"+ Log\" (shown when no mood is logged) opens `MoodLog` for a new check-in. `MoodLog` takes an optional `checkinId` to edit; a new check-in for today goes on the profile zone's today even when the dashboard passed the device's day (`utils/moodDay.ts` `moodDayInZone`, which `MoodDay` also uses so its list shows that day; its redirect to the one-entry form keeps the requested day); each face writes the shared `moodFaceValue` (Sad 10 to Excited 90, as on the web); every field is optional but at least one is required, the mood can be left unset (tapping the selected face clears it), the time row opens `TimeSheet` and stays on the current time until set, and edit mode has Delete check-in. The mood report and history chart use one value per day. It also has optional 1-5 Energy and Focus taps (`components/mood/RatingSelector`) that are sent only when changed on the screen (absent means keep, `null` means clear); `useMoodRatingsSupport` applies only to the one-entry-per-day form; it hides them when the server's mood rows lack `energy_value` and shows them when there are no rows to tell, and a save whose response lacks the key shows a one-time \"update your server\" toast. Its Progress Photos row opens `ProgressPhotos` on the diary's active date. Its Mindfulness row opens `MindfulnessLog` (`MindfulnessLogScreen`), a root-stack modal laid out like `MoodLog` and `MeasurementsAdd` (screen header, scrolling form, sticky `FooterSaveBar`, so the navigator owns Android's Back); `MindfulnessDetail` opens the same screen from the Log Mindful Session row on its Practice & Log tab and, with a `session` param, from Edit on its History tab, so it has no log form of its own. Session type names and icons come from `utils/mindfulnessSessionTypes.ts`, shared by the log screen, the detail screen and `MindfulnessCard`; the list is the shared `mindfulnessSessionTypeSchema`, and a stored type it does not have shows as Other and is kept on save. Paced Breathing and the Meditation Timer keep Finish & Save on screen while the save runs, so they go through `useSaveOncePerRun`: a run is saved once (presses are dropped until the practice is reset, taps replayed before that reset renders included), a failed save can be retried, and a save from a run closed while pending does not reset the next one. `MindfulnessLog` goes back after a save only while it is still focused, because it can be opened from a screen other than the tab host. Keep `AddSheet`'s present/dismiss refs intact to avoid Android re-present loops. Android's hardware Back in `AddSheet` does what the sheet's own Back link does: from the Exercise menu it returns to the main menu, from the main menu it closes the sheet.\n- `useNavigationActionGuard` locks navigation-triggering actions while a native-stack transition is running (idle-callback unlock on re-focus, 5s safety release) so double-taps cannot queue duplicate screens; Library create actions use it.\n- `ActiveWorkoutBar` is mounted outside normal screen trees, uses the root navigation ref, and hides itself on modal/editor routes such as food search/forms/scan/photo, exercise search, workout/activity add, measurements, mindfulness log, and barcode edit.\n- Most screens are wrapped with `withErrorBoundary(...)`; `SettingsScreen` also uses section-level recovery so settings remain reachable.\n\n## Source Map\n\n- `src/components/` - reusable UI, charts, settings rows, custom tab bar, add sheet, workout HUD, form chrome, library rows, diary rows, serving sheets, food/workout editors, fasting UI, writeback UI, and `ui/` primitives.\n- `src/components/auth/` - MFA UI shared by onboarding, setup, and reauth.\n- `src/components/compare/` - the Compare report's sentence, span control, start state, answer/checks cards and dots/bars/over-time charts, plus the since-a-habit-started pieces (`CompareSinceRows`, `CompareSinceCard`, `CompareSinceChart`); copy comes from `utils/compareCopy.ts` / `utils/compareSinceCopy.ts`, the since line steps from `utils/compareSinceLine.ts`, metric labels from `constants/compareMetrics.ts`; the question model (spans, windows, suggested questions, recents helpers) and chart data (paired dots, trend line, over-time and since chart data) come from `@workspace/shared` (`shared/src/compare/question.ts`, `shared/src/compare/chartData.ts`).\n- `src/components/glp1/` - GLP-1 tracker cards (injection log, site body map, modeled level, pen/vial inventory, titration, daily check-in) used by `Glp1TrackerScreen`; hooks in `useGlp1.ts` (query keys sit under the `medications` root so `invalidateMedicationEntryCaches` refreshes them). Body-map geometry and the contract types come from `@workspace/shared`.\n- `src/components/medications/` - dose rows, the medication list row and the adherence section: `AdherenceOverviewCard` (stat tiles, ring, 14-day bars, streak), `MedicationChecklistCard`, `MedicationDoseListCard` (due and as-needed rows over `DoseRow`), `MedicationLogCalendar` (month grid, medication filter, legend, symptom dots; owns its own entries and symptom queries), `MedicationDayLog` (the selected day's entries with edit and delete) and `useMedicationStatusColors` (theme tokens for all of them). The screens are `MedicationAdherenceScreen` (route `MedicationAdherence`, opened from the \"Adherence & log\" row on `MedicationsListScreen`) and `MedicationEntryEditScreen` (route `MedicationEntryEdit`, a modal that takes the entry as a param). All adherence, streak, checklist and calendar-status maths comes from `@workspace/shared` (`medications/adherence.ts`, shared with the web Medications page); `hooks/useMedicationAdherence.ts` feeds it one entries window ending on the selected day, and `utils/medicationAdherence.ts` holds the mobile-only helpers (as-needed selection, month shifting, edited-time conversion). The `All | Meds | Supplements` filter helpers also live in shared (`medications/subtype.ts`). Every dose change goes through `invalidateMedicationEntryCaches`, which refreshes the diary card, adherence window, month calendar and daily summary together; injection rows are edited and deleted through the injection endpoints (`useUpdateInjection`, `useDeleteInjection`). The calendar's symptom dots read `useSymptomEntries`, need the separate symptoms permission (without it the dots are simply absent), and never show `source: 'cycle'` symptoms.\n- `src/screens/` - top-level route destinations: dashboard, diary, family member/diary/meal/copy-review flows, settings, sync, logs, Whats New, fasting, food search/scan/photo, library CRUD flows, workout/activity flows, and measurement entry.\n- `src/navigation/` - navigation-level modules such as `safeScreens.tsx`, the error-boundary-wrapped screen components registered in `App.tsx`. (`FoodPhotoFlow` lives in `src/components/`.)\n- `src/hooks/` - TanStack Query hooks, auth/connection hooks, library/search/mutation hooks, measurement/water/check-in hooks, fasting hooks, workout form hooks, widget sync, query client, query keys, and cache helpers.\n- `src/services/api/` - backend clients. `apiClient.ts` handles normal API auth/proxy headers; `healthDataApi.ts`, `aiSettingsApi.ts`, food-photo estimate, and other raw fetch paths must keep auth, proxy, timeout, and session-expiry behavior aligned.\n- `src/services/healthconnect/` - Android Health Connect reads, native aggregation, transformation, enrichment, preferences, and writeback.\n- `src/services/healthkit/` - iOS HealthKit reads, statistics aggregation, transformation, background delivery, preferences, and writeback.\n- `src/services/shared/` - platform-agnostic health helpers: the `collectHealthData` / `runForegroundSync` engine both orchestrators share, the per-run workout-telemetry budget and its reuse cache, Health Connect error classification, sample downsampling, day aggregation/transformation, preference factories, and permission migration/sets.\n- `src/services/` - platform health orchestration, writeback re-exports, background sync, auto-sync coordination, diagnostics, calculations, logging, storage, theme, haptics, sounds, notifications, food photo intro, meal selection, boolean preferences, card visibility, and workout drafts.\n- `src/stores/` - Zustand stores, including the persisted active workout/rest timer store.\n- `src/utils/` - date helpers, unit conversion, food details, meal nutrition, nutrient display, workout/session helpers, fasting formatting, numeric input, concurrency, sync utilities, duplicate-press guarding, photo estimate error mapping, and rate limiting.\n- `src/constants/` - meal, exercise, fasting, and nutrient metadata.\n- JS bridges to native modules live in `src/services/` (`CalorieWidgetBridge.ts`, `ExactAlarmBridge.ts`); there is no `src/native/` directory.\n- `plugins/`, `targets/widget/`, `targets/android-widget/`, `targets/android-exact-alarm/` - Expo plugins and widget/native extension sources.\n\n## React Query And Local State\n\n- Query setup lives in `src/hooks/queryClient.ts`; keys live in `src/hooks/queryKeys.ts`.\n- Default `staleTime` is `Infinity`, so mutations must explicitly invalidate or update affected caches.\n- `refreshHealthSyncCache` invalidates daily summary, measurements, sleep (`sleepDayQueryKey` / `sleepRangeQueryKey`), and exercise history after a health sync. Sleep is in that set because a partial observer upload would otherwise sit in the diary until the app is reloaded.\n- `useRefetchOnFocus(refetch, enabled)` is the standard focus-refresh hook.\n- `useCompare(question, window)` wraps `fetchCompare` on `compareQueryKey` for a day or week question; it is disabled until the question is askable (and for a since question), refetches on focus, and reports `serverTooOld` for a 404. `useCompareSince(question, habitStart, line)` wraps `fetchCompareSince` on `compareSinceQueryKey` (`habitStart` and `line` are null until picked and then left to the server); its `serverTooOld` is a 404 or a 400 whose JSON body names `details.mode` (an older server rejecting `mode=since`), while any other 400, such as a start out of range, is an ordinary error. `compareRecentQuestions` (appPreferencesStore, newest first, max 5) is written by `CompareReportScreen` once a distinct question, since questions included, has an answer.\n- `useFoodsLibrary` is an intentional exception with an infinite query, finite stale window, and `resetQueries(...)` refreshes so focus/pull refresh reloads page 1 instead of every cached page.\n- Meal mutations invalidate meals, recent meals, search, and details; food entry creation can affect recent meals.\n- Exercise/workout preset list/search/detail invalidation belongs in their mutation hooks.\n- `useUpsertCheckIn` updates measurement queries and calls `refreshHealthSyncCache(queryClient)`.\n- `useWaterIntakeMutation` fetches `waterContainersQueryKey`, persists the selected container, and optimistically updates `dailySummaryQueryKey(date)`.\n- Active-server switches clear React Query state before refetching connection state.\n- Error-boundary retry flows call `queryClient.resetQueries()`.\n- App-local toggles live in `stores/appPreferencesStore.ts` (Zustand `persist`, single AsyncStorage key `@SparkyFitness/app-preferences`): haptics, sounds, notifications, hydration/fasting card visibility, Ask Sparky and progress photos card visibility, the Liquid Glass tab bar opt-in, the active-workout metric column, the default rest period (`defaultRestSec`, edited in `WorkoutSettingsScreen`), mood check-in reminders (`checkinRemindersEnabled`, `checkinReminderTimes`, edited in `NotificationSettingsScreen` and offered once after the first check-in), hydration reminders (`waterReminderEnabled`, `waterReminderIntervalHours`, `waterReminderWindowStart` / `waterReminderWindowEnd`, edited in `NotificationSettingsScreen`), the Health Trends layout (`healthTrendOrder` + `hiddenHealthTrends`, edited in `HealthTrendsSettingsScreen`), the Reports layout (`reportOrder`, `hiddenReports`, `reportDefaultRange`, `hiddenReportSections`, `reportNutrientOrder`, `shownReportNutrients`, edited in `ReportsSettingsScreen`), the Compare report's recent questions (`compareRecentQuestions`, newest first, max 5, written by `CompareReportScreen` and read through `knownCompareQuestions`, which drops any a later build no longer knows), and the Apple Watch layout (`watchPageOrder` + `hiddenWatchPages`, the Goals page's `watchNutrientOrder` + `shownWatchNutrients`, and `watchSetInputStyle`, all edited in `WatchSettingsScreen`). The Health Trends pair is written through the single `setHealthTrendLayout(order, hiddenKeys)` setter rather than one per field, because one drag changes order and visibility together; a saved order is reconciled against `constants/healthTrends.ts` by `resolveHealthTrendOrder`, so registering a new graph needs no `STORE_VERSION` bump. Consume via selectors (`useAppPreferencesStore((s) => s.hapticsEnabled)`) plus generated setters; non-React code reads current values through helpers like `getDefaultRestSec()`. A legacy-aware storage adapter migrates the old per-key `@HealthConnect:*` values once. These preferences never sync to the server.\n\n## Health Sync\n\n- `src/services/healthConnectService.ts` is Android orchestration; `src/services/healthConnectService.ios.ts` is iOS orchestration. They are substantial platform implementations, not thin wrappers.\n- Both orchestrators batch metric reads with `runTasksInBatches`, a concurrency of 3, and per-metric timeouts. Preserve timeout and partial-error handling.\n- Bootstrap timezone state before sync. `ensureTimezoneBootstrapped(...)` runs at startup and `healthDataApi.ts` enforces it before upload.\n- Preserve `record_timezone` and `record_utc_offset_minutes` when available.\n- Manual sync, sync-on-open, foreground-return sync, background sync, and iOS observer-triggered sync share coordination logic. Preserve claim/in-flight guards and cooldown recording.\n- Health uploads are chunked. Simple measurements use large chunks; sleep sessions use smaller session chunks; exercise/workout records are grouped by source to match server delete-then-insert behavior.\n- Sync result objects include `syncErrors`; callers should surface partial failures and avoid advancing `lastSyncedTime` when any metric read failed.\n- `backgroundSyncService.ts` uses overlap windows for sessions and day-aligned rolling windows for cumulative metrics and nutrition. Do not collapse those into one naive window.\n- On iOS, cumulative metrics should use HealthKit statistics queries, not raw sample summation.\n- On Android, cumulative metrics (`Steps`, `Distance`, `ActiveCaloriesBurned`, `TotalCaloriesBurned`, `FloorsClimbed`) use Health Connect `aggregateGroupByPeriod` once per range. Native source-priority dedup should match Health Connect UI; do not reintroduce JS `Math.max` or source allowlist dedup.\n- Android read helpers return `{ records, error }` via `readHealthRecordsDetailed` and `aggregateCumulativeMetricByDayDetailed`; legacy wrappers unwrap only records.\n- Android exercise sessions are enriched with `aggregateRecord` for active, total, and basal calories plus distance and steps over the session window. Active/total calories start scoped to `dataOrigin`, while basal energy remains unfiltered; `total - basal` is compared as an active-energy candidate. Incomplete or implausible active/total pairs retry without the origin filter so Health Connect can apply source priority. Distance and steps always stay origin-scoped; never infer workout steps from an unfiltered clock-window query.\n- iOS exercise-session steps must come from statistics attached directly to the `HKWorkout`. Do not infer workout steps by querying all step samples in the workout's clock window.\n- iOS HealthKit locked-device failures surface as database-inaccessible warnings. Do not treat these as successful empty reads.\n- `app.config.ts` grants `android.permission.health.READ_HEALTH_DATA_HISTORY` so Android can read data older than 30 days.\n- Health Connect permission migrations belong in `services/shared/healthPermissionMigration.ts`, not UI-only state. iOS does not use that version: `refreshEnabledMetricPermissions` asks HealthKit through `hasUnrequestedHealthPermissions` and requests only when an enabled type was never asked about, so bump the version for Android permission changes only.\n- Core check-in measurements use `measurementsApi.ts` and `MeasurementsAddScreen`; preserve `upsertCheckIn` omitted-vs-null semantics.\n- \"Import Full History\" (`ImportHistoryScreen`, reached from `SyncScreen`) is a one-time resumable backfill: `backfillService.ts` walks 30-day day-aligned windows newest-first from start-of-today down to a probe-derived floor (`readEarliestRecord` on both providers), one upload per window, with a per-server checkpoint in `backfillCheckpoint.ts`. It never advances `lastSyncedTime` and never runs writeback; while it runs, `isBackfillRunning()` (autoSyncCoordinator) makes background sync skip.\n- The backfill's metric set is FROZEN in its checkpoint at first run; resume uses the frozen set verbatim and toggle changes require Start Over. Quota exhaustion, locked device, and app-inactive are expected mid-run stops — the checkpoint keeps them resumable.\n\n## Health Writeback\n\n- Writeback sends Sparky diary nutrition and hydration back to Apple Health on iOS and Health Connect on Android.\n- Platform split: `services/writeback.ios.ts` re-exports `healthkit/writeback.ts`; `services/writeback.ts` re-exports `healthconnect/writeback.ts`.\n- `runWriteback()` runs after inbound sync in its own try/catch. Writeback failures must not block inbound sync results.\n- Writeback is opt-in per metric and gated on write permissions. Android production permissions include `WRITE_NUTRITION` and `WRITE_HYDRATION`; other write permissions are dev-only.\n- Imported health entries are skipped to avoid echo loops. iOS sets the app bundle id as the own-source guard; Android relies on source metadata.\n- Per-day content-signature hashing skips unchanged days. Each run deletes prior tracked UUIDs then saves fresh records; failed deletes are retried next run.\n- `HealthDataWriteback` on `SyncScreen` owns the remove flow. `BottomSheetPicker` offers all-time purge or date range through `DateRangeSheet`; both call `removeWrittenData(range)` and clear tracking.\n- Inbound iOS nutrition sync reads food correlations with a rolling nutrition lookback and upserts by `(source, source_id)` server-side.\n\n## Native Patches\n\n- `react-native-health-connect` is declared as `^4.1.3`; the installed build is patched from the repo root via `patchedDependencies` in `pnpm-workspace.yaml`.\n- Patch file: `../patches/react-native-health-connect@4.1.3.patch`, one hunk. `PermissionUtils.mapPermissionResult` maps `ExerciseRoute` and `BackgroundAccessPermission` back to JS but not `ReadHealthDataHistory`, even though `parsePermissions` accepts it on the request path. Since `requestPermission` also resolves through `mapPermissionResult`, a granted history permission can never be observed, so `ensureHistoryReadPermission` always returns false and Import Full History believes it is capped at 30 days. Reported upstream as matinzd/react-native-health-connect#276; drop the patch if that lands.\n- The older 3.5.3 patch also forced `getTimeRangeFilterLocal` in `getAggregateGroupByPeriodRequest` (DST / local-day grouping) and made `ExerciseRouteResultType` string-valued. Both are gone: 4.1.3 uses the local filter at all 21 sites, and upstream is fixing the route enum in #274 by typing the read path as a string literal union instead — a better fix than ours, so `routeNeedsConsent` keeps accepting both forms and depends on neither declaration.\n- `@bacons/apple-targets@4.0.6` is patched via `../patches/@bacons__apple-targets@4.0.6.patch`, fixing two upstream bugs. First, its xcode pass matched \"its\" extension target by type with a fall-back to any same-type target, which adopted and corrupted the expo-widgets `ExpoWidgetsTarget` on a clean prebuild; the patch scopes the match to an exact product-name hit. Second, the existing-target update path crashed every non-clean prebuild (EvanBacon/expo-apple-targets#201): removing the old build configuration list's referrers cleared `target.props.buildConfigurationList`, which the next line then dereferenced; the patch holds the list in a local and iterates a copy of its configurations so none are skipped mid-removal.\n- `@bacons/apple-targets` is held at 4.0.6 on purpose: it has a 5.x, and moving to it invalidates the patch file. `expo install --fix` does not touch either patched package (neither is an Expo SDK package), so SDK upgrades leave both patches applying cleanly.\n- `expo-health-connect` was removed. It only ever supplied an Expo config plugin, and upstream archived it: everything it did ships inside `react-native-health-connect` as of v4, which now writes both the `ACTION_SHOW_PERMISSIONS_RATIONALE` intent filter (Android 13 and below) and the `ViewPermissionUsageActivity` alias (Android 14+). Installing both causes Android build failures from duplicate classes.\n- After changing a patch or upgrading a patched package, run `pnpm install` from the repo root and then `npx expo prebuild --clean` from mobile before native validation.\n\n## Food, Meals, Units, And Photo Estimates\n\n- Food search spans local foods, online providers, meals, barcode scan, label scan, and AI photo estimates. Keep `FoodSearchScreen`, `FoodScanScreen`, `FoodEntryAddScreen`, `FoodFormScreen`, `FoodPhotoFlow`, and route params aligned.\n- `LibraryScreen` is the hub for saved Foods, Meals, recurring Meal Plans, Exercises, and Workout Presets. Full lists live in `FoodsLibraryScreen`, `MealsLibraryScreen`, `MealPlansScreen`, `ExercisesLibraryScreen`, and `WorkoutPresetsLibraryScreen`.\n- Food detail/edit flow: `FoodDetailScreen`, `FoodFormScreen`, `FoodForm`, `FoodUnitSelectorSheet`, `useFoodVariants`, `useFoodsLibrary`, `useDeleteFood`, `foodsApi`, and `utils/foodDetails.ts`.\n- `FoodForm` supports equivalent serving sizes grouped by nutrient signature, auto-scale nutrition, compatible unit conversion via `convertServingSizeOnUnitChange`, optional AI cross-category unit conversion, custom nutrients, and caller-provided `headerChildren`.\n- `EditBarcodeScreen` lets users add or remove extra barcodes for a saved food. Keep `FoodDetailScreen`, `EditBarcodeScreen`, `foodsApi`, and the `EditBarcode` route params aligned.\n- Meal templates use `MealAddScreen`, `MealDetailScreen`, `FoodSearch` / `FoodEntryAdd` with `pickerMode: 'meal-builder'`, and `services/mealBuilderSelection.ts` for pending ingredient handoff.\n- Recurring meal plans use `MealPlansScreen`, `MealPlanFormScreen`, `useMealPlans`, `mealPlansApi`, and `utils/mealPlanForm.ts`. Plans can schedule any meal already visible in the user's meal library, including family/public meals, but the plan itself remains private to its owner.\n- Workout plans (the schedule the watch's \"Scheduled today\" and the diary banner read) are created and edited from `Library > Workout plans` with `WorkoutPlansScreen`, `WorkoutPlanFormScreen`, `useWorkoutPlans`, `workoutPlansApi.ts` and `utils/workoutPlanForm.ts`, modeled on the meal plan screens. The form supports weekly and sequential plans and assigns saved workouts (presets); a single-exercise assignment made on the web is preserved on save but only editable there. Switching the schedule type clears the assignments, as the server expects new ones with a type change.\n- Logged-meal grouped diary entries use `foodEntryMealsApi`, `FoodEntryViewScreen`, and `EditLoggedMealScreen`. Preserve stored component nutrition snapshots when editing.\n- `MealTypeDetailScreen` owns single-meal-type day views and copy-to-another-day via `useCopyFoodEntries`; be careful with custom meal types and synthetic buckets.\n- External food providers use provider-agnostic v2 endpoints where possible. Provider categories and barcode support come from server config; do not hardcode provider type allowlists unless preserving an explicit fallback.\n- \"All Providers\" aggregated search (`useAllProvidersSearch`) fans one debounced term out across every active provider in parallel — one `useQueries` entry per provider, results projected in the `combine` callback for structural sharing. Providers stream in independently; a slow or failing provider must not block the others. Open Food Facts calls go through the shared rate limiter.\n- Label scan tries Apple's on-device model first: `modules/on-device-nutrition` (Swift, `FoundationModels` with the label photo as an `Attachment` plus its text from a Vision `VNRecognizeTextRequest` run in the module (greedy sampling), iOS 27+ with Apple Intelligence; compiled out under `compiler(<6.4)`) is wrapped by `services/onDeviceLabelScan.ts`, which validates the extraction (calories, macros, a per-serving size, and any optional nutrient the model filled in must appear as numbers in the returned `ocr_text`, ranges, sugars/fiber <= carbs, energy vs macros) and returns null on any failure or when the device-local `onDeviceLabelScanEnabled` preference (Settings → AI, `AiSettingsScreen`, shown only when the model is available; it also shows the active server provider, read-only) is off, so `FoodScanScreen.handleUsePhoto` falls back to the server `scanNutritionLabel`. The food form filled in from a scan carries `labelScanSource` and shows `LabelScanSourceBanner`: it says which AI read the label and offers the other one (`useRetryLabelScan` reads the kept photo from `services/labelScanSession.ts` again and replaces the form; coming back to the phone needs the setting on and the model available). The Swift side has not been compiled against the iOS 27 SDK from CI-less environments; verify on an iOS 27 device after `npx expo prebuild --clean`.\n- Photo mode is hidden in meal-builder mode because photo estimates log to the diary.\n- `FoodPhotoFlow` is a modal native stack and wraps itself in a local `KeyboardProvider`.\n- Photo availability fetches `GET /api/chat/ai-service-settings/active` through `aiSettingsApi.ts`; food photo is attempt-all, so `isFoodPhotoAvailable` gates only on a configured provider, not a specific provider type.\n- Estimation posts to `POST /api/foods/estimate-food-photo` through `estimateFoodPhoto(...)` in `externalFoodSearchApi.ts` and uses typed `FoodPhotoEstimateError` codes from `@workspace/shared`.\n- Food-photo request/response changes cross package boundaries: update shared schema and server route/service with mobile.\n- Keep `auto_scale_online_imports` separate from Open Food Facts-specific scaling preferences in `FoodSettingsScreen`.\n- Nutrient management lives under Food Settings: `CustomNutrientsScreen` / `CustomNutrientFormScreen` (create, edit, delete; renames and deletes invalidate display, goals and daily-summary caches since the nutrient name is the key), and `NutrientDisplaySettingsScreen` (`mobile` platform only, and only the view groups a mobile screen reads: `food_database`, which `FoodNutrientBreakdown` applies to the food, meal and entry nutrient lists, and `report_tabular`, which `DailyNutritionDetailsScreen` applies; `summary` and `diary` keep their Dashboard/Diary screens, and `quick_info`, `goal` and `report_chart` are not offered until a mobile screen reads them). A nutrient's goal direction is edited on the Goals screen.\n\n## Exercise, Workouts, And Fasting\n\n- Exercise and workout preset flows use `ExerciseSearch`, `PresetSearch`, detail/form screens, paginated/search hooks, mutation hooks, and shared workout payload helpers in `utils/workoutSession.ts`.\n- Session responses are discriminated unions from `@workspace/shared`: preset workouts and individual activity sessions have different shapes. Keep detail/edit screens type-safe.\n- Workout/activity drafts are persisted by `workoutDraftService`; `useWorkoutForm`, `useActivityForm`, and `useDraftPersistence` own form state.\n- Exercise selection returns via `CommonActions.setParams` and a nonce pattern through `useSelectedExercise`.\n- Adaptive coaching (#1560): every Replace action passes `replaceFor` (built by `utils/exerciseReplace.ts`) so `ExerciseSearch` opens on a ranked **Suggested** tab (`components/ExerciseAlternativesList.tsx`, `GET /api/v2/exercises/:id/alternatives`); catalog picks import through `importExercise` first. End-of-workout feedback is `components/WorkoutFeedbackCard.tsx` (on `WorkoutCompleteScreen` and `WorkoutDetailScreen`) backed by `useWorkoutSessionFeedback`, which autosaves serially so the last answer wins. `useLiveCoachingSignals` (mounted in `ActiveWorkoutScreen`) captures signals into `activeWorkoutStore.coachingSignals` (`null` = asked, no history); `resolveLiveAssumedSetValues` applies the shared `decideAdaptiveAdjustment` rules unless the entry is in `declinedAdaptive`, and `AdaptiveSuggestionBanner` always explains the change with a \"Use my usual\" undo. The `adaptive_workout_suggestions` switch in `WorkoutSettingsScreen` is an account setting; `duckMusicDuringCues` (Lower music during cues) is device-local and drives `beginCueDucking`/`endCueDucking` in `services/sounds.ts` — with it off, the audio session is never touched.\n- Bodyweight exercises (#56) use the `bodyweight_reps` modality: a set's weight is signed (+ added, − assistance, blank = body weight alone). Weight inputs parse it with `parseSignedDecimalInput` (`utils/numericInput.ts`; the base `parseDecimalInput` still rejects a sign) only when the modality is `bodyweight_reps`, including the save builders, and switch to a keyboard with a minus key. Volume and the e1RM/10RM columns go through `setVolumeKg` / `setLoadKg` in `utils/workoutSession.ts`, which delegate to the shared `effectiveLoadKg` rule, with body weight from `hooks/useBodyWeightKg.ts` (the latest check-in on or before the day, fetched only when an exercise on screen is bodyweight). Live PRs stay on the added weight, an unweighted set ranking as +0.\n- Weighted carries and loaded holds (#127) use the `weight_distance` / `weight_duration` modalities. Carry distance is typed in metres (yards when the app distance unit is miles) and converted with `carryDistanceFromKm` / `carryDistanceToKm` from `@workspace/shared` (`utils/carryDistance.ts`); always go through `setDistanceFromKm` / `setDistanceToKm` / `modalityStoresDuration` / `modalityStoresDistance` in `utils/workoutSession.ts` rather than branching on modality. Carry distance is excluded from the workout's cardio distance total. The activity edit flow (`EditableSetRow` / `buildActivitySetsPayload`, which takes the distance unit) edits weight plus seconds or distance for these types.\n- Watch support for them: a loaded hold is armed as `timed` + `weighted` (weight box beside the countdown/stopwatch), a carry as `carry` with `targetDistanceKm`; the watch shows and edits the distance in the phone's unit (metres, or yards when the distance unit is miles; the context carries `distanceUnit`, read as `effectiveCarryUnit`) while `SetValues.distanceKm` / `CompletedSet.distanceKm` stay km on the wire, and `useWatchWorkoutBridge` writes it back as `distance`. The distance keypad accepts a tenth. A pending distance edit is dropped if `distanceUnit` changes before that edit commits, and an open distance keypad is dismissed, same as a weight-unit change drops a pending weight edit. `resolveWatchSetTargets` is the single place those targets are built.\n- Rest timer state lives in `stores/activeWorkoutStore.ts`; notifications are scheduled through `services/notifications.ts`. The rest-complete ping carries a background \"Complete Set\" action (`rest-complete` category): responses are routed to `completeActiveSetIfReady` by `initWorkoutNotificationActions` (exported from the store, wired in App startup — the response listener cannot live in `notifications.ts` without a store↔service import cycle), and stale delivered pings are swept when the next rest is scheduled. Exactly one cue fires per rest: the in-app chime (`services/sounds.ts`) while the app is active, otherwise the ping's own sound. Anything that stands down in favour of the chime must test `willPlayRestCompleteSound()` (preference **and** `AppState`), never the preference alone — the notification handler also runs for frontmost-but-inactive states (screen locking, app switcher), and `markRestReady` cancels the due ping only while the app is active so the deadline flip cannot race the OS delivery. The ping ships at the `timeSensitive` interruption level so a Focus mode cannot silence it.\n- Set IDs are preserved server-side across workout edits so the active w","SparkyFitnessServer/AGENTS.md":"# AGENTS.md\n\n_Last updated: 2026-10-22_\n\nSparkyFitness Server is the backend API package for the SparkyFitness monorepo. Use this file as the primary guide for work inside `SparkyFitnessServer/`.\n\n**Quick Links for AI Tools:** See `../agent-docs/README.md` for:\n\n- `file-and-domain-reference.md` — Where to find server code by feature\n- `testing-patterns.md` — How to test routes, services, repositories, and RLS\n- `architecture-permissions.md` — Permission types and RLS patterns\n- `new-migration-checklist.md` — 8-step database change checklist\n\nIf a task also touches `shared/`, the frontend, or the mobile app, read the relevant package guide before editing outside this directory. Use `../AGENTS.md` for monorepo-level context.\n\n## Scope\n\n- This file is for package-local work in `SparkyFitnessServer/`.\n- Keep changes inside this package unless the task clearly crosses package boundaries.\n- This is the single source of truth for the package; `CLAUDE.md` just imports it via `See @AGENTS.md`.\n- Do not invent alternate boot paths, duplicate route registries, or parallel migration flows when the current startup path already covers the behavior.\n\n## Current Snapshot\n\n- Dev boot path: `pnpm start` -> `nodemon` -> `tsx index.ts`\n- `index.ts` loads `../.env`, applies file-backed secrets, runs preflight checks, calls `initializeDatabase()` for migrations and RLS policies, then imports `SparkyFitnessServer.ts`\n- Main app shell: `SparkyFitnessServer.ts`\n- Stack: Express 5, PostgreSQL via `pg`, Better Auth, Zod, TypeScript 6, Vitest 5, ESLint 10\n- Module system: ESM with `type: \"module\"` and `moduleResolution: \"NodeNext\"`\n- The package is now effectively TypeScript-first; almost all source files are `.ts`\n- Main domains: food and meal tracking, exercise logging, health and sleep data, sleep science, fasting, medications, symptom and episode tracking, mood, menstrual cycle and pregnancy, reporting, AI chat, onboarding, identity, admin tooling, and external provider integrations\n\n## Verified Commands\n\n```bash\npnpm start\npnpm run validate\npnpm run typecheck\npnpm run lint\npnpm run lint:fix\npnpm run format:check\npnpm run format\npnpm test\npnpm run test:watch\npnpm run test:coverage\npnpm run test:ci\npnpm exec vitest run tests/mealRoutes.test.ts\npnpm exec eslint routes/v2/foodRoutes.ts services/foodCoreService.ts\n```\n\n- `pnpm start` uses hot reload through `nodemon`; `nodemon.json` ultimately executes `tsx index.ts`\n- `pnpm run validate` runs typecheck, lint, and Prettier check together\n- `pnpm test` runs `vitest run`\n- The backend default port is `3010` unless `SPARKY_FITNESS_SERVER_PORT` overrides it\n- For targeted test runs, prefer `pnpm exec vitest run tests/<name>.test.ts`\n\n## Source Map\n\n- Food assistant state: owner-only preferences, recoverable tasks and operation history use `FoodAssistant.api.zod.ts` / `FoodAssistant.zod.ts` in shared. Server: `routes/v2/foodAssistantRoutes.ts`, `services/foodAssistantService.ts`, `models/foodAssistantRepository.ts`, `ai/tools/foodAssistantTools.ts`. Web: `src/components/ai/FoodAssistantState.tsx`, `src/api/Chatbot/foodAssistantService.ts`.\n\n- `index.ts` - real dev entrypoint; loads env, secrets, and preflight checks before booting the app\n- `SparkyFitnessServer.ts` - Express app shell, route mounting, Swagger/ReDoc, startup, graceful shutdown\n- `services/backgroundJobScheduler.ts` - starts every scheduled background job at startup, including the demo reset; provider syncs are listed in `services/providerSyncScheduler.ts`; `SPARKY_FITNESS_DISABLE_SCHEDULED_JOBS=true` skips them all\n- `auth.ts` - Better Auth configuration, plugins, session behavior, SSO provider syncing\n- `routes/` - primary HTTP route surface\n- `routes/v2/` - newer typed route surface; pair these changes with `schemas/`\n- `routes/v2/openFoodFactsContributionRoutes.ts` - owner-only single-food preview and explicit photo-backed publication; background contributions are disabled for this release\n- `routes/v2/symptomRoutes.ts` - generic symptom tracking (`symptoms` permission): definitions (`/custom`), the pick-list library (`/options`), entries and episodes (`/entries`, `/entries/ongoing`, `/entries/:id/end`, `/entries/:id/severity`), photos, and symptom-free days. Logic lives in `services/symptomService.ts` over `models/symptomRepository.ts` and `models/symptomOptionRepository.ts`; the request/response contract is `../shared/src/schemas/api/Symptoms.api.zod.ts`\n- `routes/reportRoutes.ts` `GET /reports/training-consistency` - the consistency view (#59): `reportService.getTrainingConsistency` reads a fixed 26 weeks of exercise entries and hands them to the shared `buildTrainingConsistency`, so web and mobile show the same streak and weekly sets (`reports` permission)\n- `routes/reportRoutes.ts` `GET /reports/training-load` - acute (7 days) against chronic (4-week average) workout load (#250): `reportService.getTrainingLoad` reads every logged workout through `reportRepository.getTrainingLoadSessions` and the person's heart rate zones (`heartRateZoneService.resolveZonesForUser`), and hands them to the shared `computeTrainingLoad` (`reports` permission)\n- `routes/v2/reportRoutes.ts` - weekly alcohol rollup and the zero-padded hydration/caffeine/alcohol range used by the Trends charts (`reports` permission)\n- `routes/v2/reportRoutes.ts` `GET /compare` - does A go with B (`/api/v2/reports/compare`, owner-only via `requireSelfActor`): parses `compareQuerySchema`, `services/compareService.ts` (`getCompare`) loads both metrics through `models/compareRepository.ts` (today and the current week never counted) and hands them to the shared analysis in `../shared/src/compare/` (`stats.ts`, `metrics.ts` catalog, `analyze.ts` day/week pairing); week mode uses `utils/firstDayOfWeek.ts` and training load is weighted by the person's zones (`heartRateZoneService.resolveZonesForUser`); `mode=since` (before vs since a habit started) parses `compareSinceQuerySchema` instead and calls `getCompareSince`, which hands the shared analysis in `../shared/src/compare/since.ts` the habit over the last 730 days plus two weeks (the analysis holds a suggested start to the last 730 days, `SINCE_START_MAX_DAYS_AGO`, and offers none for a habit already kept at that limit); contract `../shared/src/schemas/api/Compare.api.zod.ts`. The AI tool `ai/tools/compareTools.ts` (`sparky_compare_metrics`, `reports` category, read-only for MCP keys) asks the same two service functions through the shared query schemas and renders the result as text; its English metric names and wording live in that file\n- `routes/v2/nutritionKineticsRoutes.ts` - active-caffeine estimate and bedtime cutoff (`diary` permission)\n- `routes/v2/workoutCoachingRoutes.ts` - adaptive coaching (#1560): session feedback (`workout_feedback`), the per-user `adaptive_workout_suggestions` setting (owner-only write), and recent-history signals (`diary` permission). `GET /v2/exercises/:id/alternatives` (ranked substitutes) lives in `routes/v2/exerciseRoutes.ts`\n- `routes/v2/routeRoutes.ts` - the person's saved routes (`/api/v2/routes`: `GET` summaries, `POST` a drawn route from `points` (+ optional `cues`), `POST /import-gpx` with the file text, `POST /from-activity/:exerciseEntryId` from a recorded GPS track, `GET/PATCH/DELETE /:id` (PATCH renames, changes the activity or replaces the line: `points` + optional `cues` go through `prepareRoute`, which recomputes distance and climb in the same UPDATE and leaves `source`, `source_exercise_entry_id` and `created_at` alone; cues need `points` and every cue index must be below the point count), `GET /:id/gpx` download; 200 per person); owner-only (`requireSelfActor`, `routes` owner policy). `models/routeRepository.ts` stores them; `utils/routePrep.ts` (`prepareRoute`) drops repeated points, measures distance and climb on the full track, thins to 2000 points and re-points cues. Geometry, GPX read/write and the contract are shared: `../shared/src/utils/routeGeometry.ts`, `routeGpx.ts`, `../shared/src/schemas/api/Route.api.zod.ts`\n- `routes/v2/routeRoutes.ts` generation endpoints - `GET /api/v2/routes/generate/capabilities` and `POST /api/v2/routes/generate` (preview only, never saves; owner-only; 10 requests per user per 10 min via `utils/userRateLimiter.ts`). Orchestration is `services/routeGenerationService.ts` (loop resizing, shared `prepareRoute` thinning); the engine sits behind `RoutingProvider` in `services/routing/`. `routingConfig.ts` `resolveRoutingProvider(userId)` picks the engine per request: the user's active `valhalla` external provider first (own, then admin-public, then family-shared via `externalProviderRepository.getActiveProviderWithKeyByType`), else none. The base URL is re-checked with the food-provider network policy and DNS-guarded for non-admin owners, the key comes from the encrypted `app_key` and is sent as `api_key`, and nothing is cached so a new provider applies at once. There are no environment variables for routing. Every engine request carries a `SparkyFitness/<version>` `User-Agent`. The type is seeded by migration `20261021120200_add_valhalla_routing_provider_type.sql` and registered in `BASE_URL_FETCHING_PROVIDER_TYPES` in `externalProviderService.ts`; `valhallaProvider.ts` talks to the engine, pure `valhallaManeuvers.ts` decodes polylines and maps maneuvers to cues, pure `loopPlanner.ts` plans loops. `GET /generate/capabilities` reports `{ available, maxAlternatives: 1, style (all false), stops: false, source: 'provider' | null }` from `getRouteGenerateCapabilities`; the server returns one route per request and `POST /generate` answers `{ route }`. Saving a generated route posts it to `POST /api/v2/routes` with `source: 'generated'` (the only source a client may choose besides `drawn`; migration `20261021120100_allow_generated_route_source.sql` widens the check). Add an engine by implementing `RoutingProvider` and a case in `routingConfig.ts`; never log the provider error object (it can carry the key).\n- `routes/v2/runProgramRoutes.ts` - the person's run program and place in it (`/api/v2/run-program`: `GET`, `PUT` to start, switch on or off, move, or set the Diary schedule (`run_days`, `start_date`, `end_date`), `POST /complete` which also records the person's local `day` as `last_done_date` and, given `entry_id`, appends `{index, entry_id, at}` to `completion_log` (newest 200 kept; cleared on a program switch) so the app can show a diary run as part of the program, `POST /effort` which saves a 1 to 10 rating for a workout already done (`rateRunProgramWorkout`: under the row lock, replaces an earlier rating of the same index, keeps the last 40 in `effort_log`, refused for a workout not yet done or another program; a program switch clears it); the tool's `get_run_program` lists recent ratings and, through shared `suggestFromEffort`, notes two hard or two easy in a row); `POST /realign` rebuilds the rest of the plan to end on the goal day through shared `realignProgram`/`applyRealign` (`../shared/src/utils/runProgramRealign.ts`): without `confirmed: true` it only returns the preview (and the options when the goal is unrealistic) and saves nothing; confirmed, it recomputes under the `adjustRunProgram` row lock (the callback also receives the whole stored `program`), refuses a stale preview `token` with 409 and an unrealistic goal without `force` with 400; the person's local `today` comes from the body or their timezone. After a realignment the stored workouts no longer line up with the built-in definition, so adjusts measure against `builtInBaseline` (built-in workouts only while the lengths match). Owner-only (`requireSelfActor`, `run_programs` owner policy). The person's own plans live in `custom_run_programs` (`GET/POST /custom`, `PUT/DELETE /custom/:id`, 30 per person, up to 364 workouts of timed steps, program id `custom:<uuid>`): starting one through `PUT` copies it into `run_programs` (with `name`/`activity`), editing one also updates the active copy and keeps the place, deleting keeps the active copy. `models/runProgramRepository.ts` seeds the stored `workouts` from the built-in program in `../shared/src/utils/runPrograms.ts`, completes the due workout in one statement so a repeat cannot skip one, and `adjustRunProgram` applies a change under a row lock with a log entry. The change rules (only upcoming workouts, running eased up to 30% or raised at most 10%) are `../shared/src/utils/runProgramAdjust.ts`, shared by the AI tool `ai/tools/runProgramTools.ts` (`sparky_manage_run_program`; every change needs `confirmed=true`; `set_run_program_schedule` changes the Diary days (names or 0 to 6), first day and goal day through the same upsert; `realign_run_program` (fields `force`, `distance`, `confirmed`) previews the same plan and applies it only with `confirmed=true`, and says nothing was changed for an unrealistic goal; `get_run_program` adds a line when the plan has drifted from the goal day; only `get_run_program` is read-only for MCP keys, and the realign POST is not on the read-only allowlist in `utils/apiKeyScope.ts`) and the contract is `../shared/src/schemas/api/RunProgram.api.zod.ts`\n- `routes/v2/heartRateZoneRoutes.ts` - the person's heart rate zone settings (`GET`/`PUT /api/v2/heart-rate-zones`; max and resting heart rate, `percent_max` / `heart_rate_reserve` / `custom` method, custom floors; `reset: true` restores defaults). Owner-only (`requireSelfActor`, also on reads). Logic in `services/heartRateZoneService.ts` over `models/heartRateZoneRepository.ts` (columns on `user_preferences`); `resolveZonesForUser(userId, observedMaxBpm?)` is what stored time in zone (`healthDataHandlers.persistWorkoutTelemetry`, `exerciseEntryService.attachWatchTelemetryToExerciseEntry`) and training load use. Zone maths is `../shared/src/utils/heartRateZones.ts`; the contract is `../shared/src/schemas/api/HeartRateZones.api.zod.ts`. Stored `exercise_entry_hr_zones` rows carry `zone_method` (`device` for Garmin) and are never recomputed when settings change.\n- `routes/v2/mindfulnessRoutes.ts` - mindfulness & meditation sessions tracking (`checkin` permission): day summaries (`/day-summary`), session logs and telemetry (`/entries`, `/entries/:id`). Logic lives in `models/mindfulnessRepository.ts`; the request/response contract is `../shared/src/schemas/api/Mindfulness.api.zod.ts`\n- `routes/v2/shoeRoutes.ts` - running-shoe inventory (`diary` permission): CRUD on `/api/v2/shoes` and `PUT /shoes/assignments/:exerciseEntryId` to link or unlink a diary entry (`exercise_entries.shoe_id`). Logic lives in `models/shoeRepository.ts`; mileage is `starting_distance_km` plus the sum of linked entries' `distance` (km); the contract is `../shared/src/schemas/api/Shoes.api.zod.ts`\n- `routes/auth/` - auth-specific route fragments mounted through `routes/authRoutes.ts`\n- `services/` - business logic and orchestration\n- `models/` - PostgreSQL repositories and persistence helpers\n- `middleware/` - auth, permissions, uploads, and shared Express middleware\n- `utils/uploadsPath.ts` - the uploads root plus the resolver and containment guard for stored `file_path` values; use it instead of re-deriving `SPARKY_FITNESS_CUSTOM_UPLOADS_DIRECTORY`\n- `utils/oauthState.ts` - server-issued single-use OAuth `state` nonces for provider linking (`issueOAuthState`, `persistOAuthState`, `claimOAuthState`); use it instead of hand-rolling a state value\n- `utils/outboundHttp.ts` - process-wide outbound HTTP defaults, applied from `index.ts`: the axios request timeout and the per-address-family connect attempt timeout (`net.setDefaultAutoSelectFamilyAttemptTimeout`). Both are fixed constants on purpose - do not add env overrides, and read the sizing note there before changing either, because the two values interact\n- `utils/errors.ts` - `ValidationError` plus `describeError(error)`; prefer it over `error.message` when logging any caught value, because an `AggregateError` or a non-Error throw renders as an empty string\n- `middleware/requireSelfMiddleware.ts` - `requireSelfActor`, which rejects a switched/delegated context outright; attach per-route to account-linking routes\n- `integrations/` - provider adapters and ingest pipelines\n- `schemas/` - Zod route schemas\n- `types/` - TypeScript declarations, including `Express.Request` augmentation\n- `db/` - pool management, grants, migrations, and RLS policies\n- `config/` - logging and Swagger config\n- `utils/` - startup helpers, CORS, permissions, timezone loading, OIDC helpers, migration helpers\n- `ai/` - AI provider configuration (`config.ts`), the unified provider-dispatch helper (`providerDispatch.ts`), and the in-process chatbot tool registry (`ai/tools/`)\n- `security/` - encryption utilities (`encryption.ts`)\n- `validation/` - legacy express-validator rules for a few older routes (new routes use Zod schemas)\n- `constants/` - shared constants and supporting package data\n- `tests/` - Vitest suites plus a few utility scripts\n- `devdocs/` - local notes and debugging artifacts when present\n\nWhen searching, ignore noisy/generated directories unless you explicitly need them:\n\n- `node_modules/`\n- `coverage/`\n- `uploads/`\n- `temp_uploads/`\n- `backup/`\n- `mock_data/`\n\n## Architecture\n\n### Boot and App Shell\n\n- `index.ts` is the true local boot path used by `pnpm start`; do not bypass it for normal development because it performs env loading and preflight work\n- `SparkyFitnessServer.ts` creates the Express app, configures static upload serving, mounts auth interception, registers routes, exposes API docs, starts background jobs through `services/backgroundJobScheduler.ts`, and handles graceful shutdown\n- Startup order matters:\n  - `index.ts`: await `initializeDatabase()`, which applies pending migrations and then reapplies `db/rls_policies.sql` under a PostgreSQL advisory lock, **before** importing `SparkyFitnessServer.ts` (and therefore `auth.ts`)\n  - upsert env-configured OIDC provider\n  - mount Better Auth\n  - sync trusted SSO providers\n  - register cron jobs\n  - optionally promote `SPARKY_FITNESS_ADMIN_EMAIL` to admin\n  - start listening\n- Public API docs live at:\n  - `/api/api-docs/swagger`\n  - `/api/api-docs/redoc`\n  - `/api/api-docs/json`\n- If you change public endpoints, keep Swagger JSDoc and `config/swagger.ts` coverage accurate\n\n### Environment and Secrets\n\n- Runtime `.env` is expected at `../.env`\n- The tracked template lives at `../docker/.env.example`\n- `utils/secretLoader.ts` loads `*_FILE` secrets before preflight validation\n- Three layers decide whether a variable has to be set, and they are easy to confuse:\n  1. **`utils/preflightChecks.ts` refuses to start** without these four, because none has a safe default:\n     - `SPARKY_FITNESS_DB_PASSWORD`\n     - `SPARKY_FITNESS_FRONTEND_URL`\n     - `SPARKY_FITNESS_API_ENCRYPTION_KEY`\n     - `BETTER_AUTH_SECRET`\n  2. **`preflightChecks.ts` fills in a default** for `SPARKY_FITNESS_DB_HOST` (`sparkyfitness-db`), `SPARKY_FITNESS_DB_NAME` (`sparkyfitness_db`), `SPARKY_FITNESS_DB_USER` (`sparky`) and the two app-role variables, logging which one it defaulted. These matter only outside Compose, which supplies them itself.\n  3. **`docker/docker-compose.prod.yml` supplies a value** for almost everything via `${VAR:-default}`, so a Compose deployment only ever has to set the four in (1). Keep the defaults in (2) identical to Compose's: a value that differs between them silently points the server at a database other than the one Compose created.\n- `SPARKY_FITNESS_APP_DB_USER` and `SPARKY_FITNESS_APP_DB_PASSWORD` are soft-required: preflight defaults the user to `sparky_app` and mints a password when absent, and `utils/dbMigrations.ts` creates the role or re-syncs its password so the two always match. It probes a connection as that role first, so an externally pre-created role is left alone and the owner does not need `CREATEROLE` — but only while `SPARKY_FITNESS_APP_DB_PASSWORD` still authenticates. If it is absent, preflight mints a new one, the probe fails, and the `ALTER ROLE` does need `CREATEROLE`; an externally managed database should therefore set both app variables explicitly. The probe only reports failure for an authentication rejection (`28P01`/`28000`); any other connection error propagates rather than being misread as a stale password. Both assignments must stay in `preflightChecks.ts`, because `db/poolManager.ts` freezes its credentials at module load\n- `BETTER_AUTH_SECRET` is mandatory. It signs session cookies and encrypts stored 2FA/TOTP secrets, so a value that changes between restarts logs every user out and permanently locks out anyone with 2FA enabled. Startup used to mint a throwaway one when it was missing, which made exactly that happen silently; it now fails preflight instead\n- Preflight also refuses to start when `BETTER_AUTH_SECRET` or `SPARKY_FITNESS_API_ENCRYPTION_KEY` still holds a template placeholder (a value starting with `changeme` or `replace_with`, as shipped in `docker/.env.example` and `docker/.env.simple.example`), and only warns for `SPARKY_FITNESS_DB_PASSWORD` because Compose initialises Postgres with it. Keep new template secrets on one of those prefixes so the check covers them. It also fails when `BETTER_AUTH_SECRET` base64-decodes to an empty key (Better Auth accepts an empty Buffer) and warns under 32 decoded bytes\n- Common operational toggles include `SPARKY_FITNESS_SERVER_PORT`, `SPARKY_FITNESS_ADMIN_EMAIL`, `ALLOW_PRIVATE_NETWORK_CORS`, `ALLOW_PRIVATE_NETWORK_AI`, `ALLOW_PRIVATE_NETWORK_FOOD_PROVIDERS`, `SPARKY_FITNESS_EXTRA_TRUSTED_ORIGINS`, and `BETTER_AUTH_URL`\n- User-configured self-hosted food providers (Mealie/Tandoor/Norish) can point `base_url` at a private/internal address only for admins by default; a non-admin on a multi-user server is blocked unless the operator opts in, either with the admin `allow_private_network_food_providers` toggle (Admin > Global Provider Settings) or `ALLOW_PRIVATE_NETWORK_FOOD_PROVIDERS=true`. This mirrors the AI policy (a single-user self-host is an admin, so their LAN recipe server works with no config). Enforced by `utils/outboundUrlPolicy.ts` at provider save time in `services/externalProviderService.ts`. Separate from the AI toggle by design\n- The admin `allow_private_network_ai` toggle (Admin > Global AI Settings), or `ALLOW_PRIVATE_NETWORK_AI=true`, lets non-admin users use custom AI service URLs (`custom`/`ollama`/`openai_compatible`) that resolve to private/internal addresses; default off is an SSRF guard enforced by `utils/outboundUrlPolicy.ts` at save/test time and again in the runtime guarded fetch path. Current admins and global admin-created AI settings can use private URLs for self-hosted providers like Ollama\n- **Call `resolveAiNetworkPolicy` / `resolveFoodProviderNetworkPolicy`, not the `derive*` forms.** The sync `derive*` functions only see the env var; the async `resolve*` wrappers also consult the admin toggle (and only hit the database when the sync answer would be a denial). The `derive*` exports stay for unit tests and for the resolvers themselves\n\n### TypeScript and Module Conventions\n\n- This package is now almost entirely TypeScript; new source files should be `.ts`\n- Keep local relative imports using `.js` extensions from TypeScript files, for example `import foo from './foo.js'`\n- `eslint.config.js` enforces file extensions in imports\n- `tsconfig.json` uses `NodeNext`, `noEmit`, and `allowJs: false`\n- `@workspace/shared` resolves directly to `../shared/src/index.ts` here and in Vitest\n- Avoid using `any` declarations in models, repositories, and integration services (e.g. `integrations/fatsecret/fatsecretService.ts`). Instead, use base datatypes (like `string`), proper types/interfaces, or import strict type schemas directly from `@workspace/shared`.\n- New public endpoints should include TypeScript code, Zod validation, and automated tests\n\n### Logging\n\n- Use `log(level, message, ...args)` from `config/logging.ts`; levels are `'debug'`, `'info'`, `'warn'`, and `'error'`\n- Never use `console.error` (or other `console.*`) in application code\n- Exception: fatal boot diagnostics in `index.ts` and `utils/preflightChecks.ts` print with `console.error` (alongside `log('error', ...)`), because `log()` is suppressed at `SILENT` and the operator must still see why the server refused to start\n- `SPARKY_FITNESS_LOG_LEVEL` controls verbosity (`DEBUG`, `INFO`, `WARN`, `ERROR`, `SILENT`)\n\n### Database and RLS\n\n- Use `getClient(userId, authenticatedUserId?)` from `db/poolManager.ts` for normal user-scoped queries\n- `getClient(...)` sets `public.set_app_context(...)`; that is what makes row-level security work correctly\n- Use `getSystemClient()` only for admin, migration, startup, or policy-management work that intentionally bypasses RLS\n- Always release database clients in a `finally` block\n- To learn a table's current shape, read `../shared/src/schemas/database/<Table>.zod.ts` (one small Zod file per table) instead of reading `../db_schema_backup.sql` or reconstructing it from the 185 migration files\n- New migrations belong in `db/migrations/` and must use `YYYYMMDDHHMMSS_description.sql`\n- **Never manually edit `../db_schema_backup.sql`** — after merge, CI regenerates it from the migrations and opens an automated sync PR (`.github/workflows/schema-backup.yml`). Do not commit copies generated from a local database.\n- If you add a new table or change user-visible access behavior, follow `../agent-docs/new-migration-checklist.md`. In short, you MUST:\n  1. Add/modify the RLS policies in `db/rls_policies.sql`.\n  2. Update the user-facing documentation in `../docs/src/features/family-friends-sharing.md`.\n  3. Update the developer-facing documentation in `../docs/src/developer/database-security-tiers.md` to define its security tier (Tier 1, Tier 2, or Tier 3).\n  4. Add or update the matching Zod schema in `../shared/src/schemas/database/`.\n- Keep future schema-startup steps in `utils/initializeDatabase.ts` and pass its shared client through all database work. The lock and schema work must use the same connection so initialization cannot continue on another connection after the lock-owning session is lost. Do not create alternate migration mechanisms.\n- Migrations run from `index.ts`, **before any application module is imported**, and via dynamic `await import()`. Both details are load-bearing: Better Auth validates the schema eagerly at `auth.ts` module scope and caches a mismatch for the life of the process (issues #2469 / #2470), and `db/poolManager.ts` builds its pools at module load, so a static import would be hoisted above the env/secret loading. `tests/bootOrder.test.ts` guards this\n\n### Uploads: Public vs Sensitive\n\n- `SparkyFitnessServer.ts` serves the uploads root publicly at `/uploads` and `/api/uploads`; both are in `publicRoutes`, so `authenticate` never runs on them\n- Sensitive subtrees are **denied on the static mount** and served instead by an authenticated, owner-checked per-id route. Three exist today:\n  - `check-in` -> `GET /api/measurements/check-in-photos/file/:id` (delegatable via the `checkin` permission)\n  - `pregnancy` -> `GET /api/v2/pregnancy/photos/file/:id` (owner-only; deliberately **no** `checkPermissionMiddleware`, because reproductive-health data is never delegated)\n  - `symptoms` -> `GET /api/v2/symptoms/photos/file/:id` (delegatable via the `symptoms` permission)\n- Adding a sensitive upload subtree means adding its directory name to `SENSITIVE_UPLOAD_SUBTREES` in `SparkyFitnessServer.ts` **and** adding an authenticated file route; the deny rule matches the decoded, normalized path, because a prefix match on the raw URL is bypassable with `..%2f`\n- Responses for these domains omit `file_path`: the on-disk layout is a server detail and clients address photos by id\n- `tests/uploadsStaticMount.test.ts` guards both the deny behavior and the fact that the deny rule is registered before `express.static`\n\n### Auth and Request Context\n\n- Better Auth is configured in `auth.ts` and mounted under `/api/auth`\n- `SparkyFitnessServer.ts` intercepts `/api/auth*` requests before the normal request logger and has special handling for discovery routes and sign-out cookie cleanup\n- `middleware/authMiddleware.ts` populates:\n  - `req.userId`\n  - `req.authenticatedUserId`\n  - `req.originalUserId`\n  - `req.activeUserId`\n  - `req.user`\n- `req.userId` is the active RLS target; `req.authenticatedUserId` is the logged-in actor\n- Family and delegated access flow through `middleware/checkPermissionMiddleware.ts`, `middleware/onBehalfOfMiddleware.ts`, and the auth middleware’s active-user switching\n- `checkPermissionMiddleware(permissionType)` guards routes; permission types are `'diary'`, `'reports'`, `'checkin'`, `'medications'`, and `'symptoms'` (GET resolves to the read-only `*_read` variant)\n- If you change auth behavior, check both cookie-backed sessions and API key flows\n- API keys carry a scope in `api_key.permissions` (Better Auth format, `@workspace/shared` `ApiKeys.api.zod.ts`): `{ sparky: [\"read\"] }` is read-only, `{ sparky: [\"read\", \"write\"] }` or no permissions is full access. The scope is set only by `POST /api/identity/user/generate-api-key`, because Better Auth refuses `permissions` from a client. `authenticate` sets `req.apiKeyReadOnly` and answers 403 for anything but GET/HEAD/OPTIONS plus the POST allowlist in `utils/apiKeyScope.ts`; the `/api/auth` interceptor refuses mutations from read-only keys via `middleware/readOnlyApiKeyGuard.ts`; `/mcp` filters tools and actions with `ai/mcp/toolAccess.ts`. A new MCP tool or action is unavailable to read-only keys until it is classified there\n\n### Dates, Day Strings, and Timezones\n\n- Prefer the shared helpers exported by `@workspace/shared` for day-string and timezone-aware logic\n- Common server-side helpers include `todayInZone`, `instantToDay`, `instantForDay`, `dayToUtcRange`, `dayRangeToUtcRange`, `localDateToDay`, `addDays`, `compareDays`, and `isDayString`\n- Load the user timezone through `utils/timezoneLoader.ts` before deriving \"today\", bucketing events by day, or building date ranges from user context\n- Treat `YYYY-MM-DD` values as calendar-day strings, not UTC-midnight timestamps\n- Avoid `toISOString().split('T')[0]` for user-facing or business-logic dates; it silently shifts dates near timezone boundaries\n- If you touch older code that still uses UTC split patterns, prefer migrating that path to the shared helpers instead of copying the pattern forward\n- Timezone/date regression coverage already exists in:\n  - `tests/timezone.test.ts`\n  - `tests/dateShifting.test.ts`\n  - `tests/measurementService.timezone.test.ts`\n\n### Integrations and Background Work\n\n- Provider-specific adapters live under `integrations/`; coordinating logic usually lives in `services/` and persistence in `models/`\n- Current adapters span food/nutrition (OpenFoodFacts, FatSecret, Nutritionix, USDA, Mealie, Tandoor, Norish, SwissFood, Yazio), fitness devices (Garmin Connect sync plus FIT file import via `integrations/garminfit/` + `services/fitImportService.ts`, Withings, Fitbit, Oura, Polar, Strava, COROS, Hevy), exercise databases (Wger, FreeExerciseDB), and health-data import (Google Health, generic/mobile health data)\n- Scheduled jobs currently include backups, session cleanup, and hourly sync loops for Withings, Garmin, Fitbit, Oura, Polar, Strava, and COROS\n- Integration work often spans route, service, repository, cron, and external-provider settings code; inspect the whole path before calling the work complete\n- **OAuth linking (`/authorize`, `/callback`) is self-only, and `state` is a server-issued single-use nonce.** Never derive a user id from a callback request body, and never gate an authorize route with `checkPermissionMiddleware('diary')` — on GET that resolves to `diary_read`, which would hand a read-only delegate the owner's decrypted OAuth client id. Use `requireSelfActor` plus `utils/oauthState.ts`. Withings and Polar follow this pattern; Oura, Fitbit and Strava are self-only but still send `state = userId` and ignore it on callback (tracked follow-up)\n\n### AI Services\n\n- AI calls go through the Vercel `ai` SDK (v7) with provider adapters for OpenAI, Anthropic, and Google, plus OpenAI-compatible, Mistral, Groq, OpenRouter, and Ollama service types\n- `ai/config.ts` holds default model and vision-model selection per provider; `ai/providerDispatch.ts` is the unified dispatch helper used by chat, food-photo analysis, nutrition-label scan, and unit conversion\n- Prefer routing new AI features through `providerDispatch.ts` instead of calling provider SDKs directly\n- Web and mobile voice transcription use `POST /api/chat/transcribe` in `routes/chatRoutes.ts` and `services/voiceTranscriptionService.ts`; its multipart fields, response, MIME types, and 10 MB limit come from `shared/src/schemas/api/VoiceTranscription.api.zod.ts`. Audio stays in memory, uses actor-scoped AI settings with `supports_audio_input`, and all outbound calls use `createGuardedFetch`. Voice and chat providers are selected independently; transcription never executes chatbot tools or writes diary entries. `voice_input_enabled` and `active_voice_ai_service_id` in user preferences synchronize voice UI across clients; null selects System recognition, and disabling voice preserves the selection. Preference writes validate these fields using the shared voice-preferences schema.\n- Chatbot tool calls run in-process through the registry in `ai/tools/`\n- `ai/tools/index.ts` exposes `buildChatbotTools(userId, tz)`, composing the per-domain builders (`build<Domain>Tools` in `ai/tools/<domain>Tools.ts`); handlers close over the authenticated user — so two-actor services receive `(userId, userId, ...)` — and the user's IANA timezone, used for \"today\" defaults and day bucketing\n- Tool handlers follow a fixed contract: publish a flat Zod schema, validate with a strict union `safeParse` inside `execute`, orchestrate through existing services and repositories, and never throw - errors come back as `ERRORS.*` strings from `ai/tools/errors.ts`\n- Tool output text is a parity contract with the MCP tool set; golden tests in `tests/chatbotTools*.test.ts` assert exact returned strings, so do not reword tool output casually\n\n## Testing and Validation\n\n- Test runner: Vitest, not Jest\n- Auto-discovered test files match `tests/**/*.test.ts`\n- `tests/check_routes.ts` and `tests/*.script.ts` are utility scripts, not normal test suites\n- For route or contract work, targeted `supertest`-based Vitest tests are the normal validation path\n- Prefer `pnpm run typecheck` after touching `routes/v2/`, `schemas/`, `types/`, or shared request/response contracts\n- Prefer `pnpm run lint` after multi-file edits; if unrelated package-wide issues make that noisy, run targeted `pnpm exec eslint <paths>` on the touched files before stopping\n- Use `pnpm run test:coverage` after broad service, route, repository, middleware, or auth refactors\n\n## Quick Routing\n\n- Weekly weight-change calorie goals: `services/preferenceService.ts` validates the signed `goal_mode_weekly_change_kg` preference (negative loses, positive gains; active only for `goal_mode = 'weekly'`), `models/preferenceRepository.ts` persists it, and `services/goalService.ts` plus `services/calorieBalanceService.ts` pass it to the shared calorie adjustment functions. Device Projection applies the daily weekly-rate adjustment once to device TDEE; percentage goals keep their existing behavior.\n\n- Startup, env, or deployment issue:\n  inspect `index.ts`, `SparkyFitnessServer.ts`, `utils/secretLoader.ts`, `utils/preflightChecks.ts`, and `config/logging.ts`\n- Auth, session, MFA, or API key issue:\n  inspect `auth.ts`, `middleware/authMiddleware.ts`, `routes/authRoutes.ts`, and `routes/auth/`\n- Migration, RLS, or permission issue:\n  inspect `db/migrations/`, `db/rls_policies.sql`, `db/poolManager.ts`, `utils/applyRlsPolicies.ts`, and the permission middleware/helpers\n- Public v2 contract issue:\n  inspect the matching file in `routes/v2/` plus the related Zod schema in `schemas/`\n- Food, barcode, or external provider issue:\n  inspect the relevant `integrations/*` code, then the matching service and repository files\n- Open Food Facts publication:\n  inspect `services/openFoodFactsManualContributionService.ts`, `integrations/openfoodfacts/openFoodFactsContribution.ts`, and `constants/openFoodFacts.ts`; retained automatic queue code is dormant and needs a new migration before a future release can activate its triggers\n- Heart rate zone, max heart rate or time-in-zone issue:\n  inspect `../shared/src/utils/heartRateZones.ts`, `services/heartRateZoneService.ts`, `routes/v2/heartRateZoneRoutes.ts`, then `services/healthDataHandlers.ts` (`persistWorkoutTelemetry`) and `services/exerciseEntryService.ts` (watch telemetry) for the write paths\n- Health data or date bucketing issue:\n  inspect `integrations/healthData/healthDataRoutes.ts`, `services/measurementService.ts`, and `utils/timezoneLoader.ts`\n- Attaching heart rate to an already-existing exercise entry (e.g. from a paired Apple Watch's live workout tracking, `SparkyFitnessMobile/src/hooks/useWatchWorkoutBridge.ts`):\n  inspect `POST /exercise-entries/:id/watch-telemetry` in `routes/exerciseEntryRoutes.ts`, `services/exerciseEntryService.ts`'s `attachWatchTelemetryToExerciseEntry`, and `models/exerciseEntry.ts`'s `applyWatchTelemetryAtomically`. Distinct from `services/healthDataHandlers.ts`'s `persistWorkoutTelemetry`, which creates a new entry as part of importing a whole synced workout (HealthKit/Health Connect/Garmin) — this route only fills in avg/max HR, `exercise_entry_hr_zones` and `calories_burned` on an entry that already exists, and it also stores the posted series as `heart_rate` samples tagged with the entry (`storeHeartRateSeries`, source `SparkyFitness Watch`, `merge` over the workout's own span but only this entry's samples, and only after that snapshot was accepted under the row lock, best effort) because the heart rate chart of an entry with no GPS track reads those samples and would otherwise wait on a Health sync. `activeEnergyKcal` is a real measurement from the watch and is written to BOTH `calories_burned` (the figure the diary totals) and `active_calories` (a telemetry column the ordinary entry update preserves). That second write is what makes the measurement survive a later edit: `resolveEditedCaloriesBurned` in `services/exerciseService.ts` prefers a stored `active_calories` over the recomputed duration-and-sets estimate, so editing a note or a weight no longer replaces what a watch measured with a formula. A client-sent `calories_burned` still overrides both. Both body fields are individually optional (HealthKit permissions are per type) and the model does a partial UPDATE, so a calories-only post must not blank heart rate an earlier post attached.\n  Workout effort (a 1-10 rating of the whole workout, `exercise_entries.workout_effort`, CHECK 1-10): it is one of `EXERCISE_ENTRY_TELEMETRY_COLUMNS`, so a synced workout carries it as `telemetry.workout_effort` through the health-data allowlist (HealthKit sends the person's own Apple rating) and the model's insert, update and select pick it up with no per-column code. `POST /exercise-entries/:id/effort` (`setExerciseEntryEffort`, body `{rating}`) is the write for ratings given afterwards, on the watch: it sets the entry and every other entry with the same `exercise_preset_entry_id`, so a strength session reads the same from any exercise and the phone needs only one entry id. `GET /api/exercise-stats/query` returns it as `workoutEffort`.\n- Attaching a phone-recorded GPS track to an existing exercise entry (mobile `RecordActivityScreen`):\n  inspect `POST /exercise-entries/:id/gps-track` in `routes/exerciseEntryRoutes.ts` and `services/exerciseEntryService.ts`'s `attachGpsTrackToExerciseEntry`, with the request schema `attachExerciseEntryGpsTrackRequestSchema` in `shared/src/schemas/api/ExerciseEntries.api.zod.ts`. It stores the route and laps (laps arrive as bare windows and are derived by `deriveLaps`) and backfills speed/elevation/moving-time with `deriveWorkoutTelemetry`, all in one transaction. Both inserts upsert per entry, so a retried post replaces the track. Use this rather than `/api/health-data` for app-recorded activities: the health-data path range-deletes a source's entries for the affected days.\n- Attaching laps to an entry with no GPS track (an indoor run recorded on the watch):\n  inspect `POST /exercise-entries/:id/laps` in `routes/exerciseEntryRoutes.ts` and `services/exerciseEntryService.ts`'s `attachLapsToExerciseEntry`, with `attachExerciseEntryLapsRequestSchema` in `shared/src/schemas/api/ExerciseEntries.api.zod.ts`. The client sends the marked windows with the distance and average heart rate it worked out; the server derives duration and speed and upserts by lap index into `exercise_entry_laps`, so a retry replaces them. Entries with a track get their laps from `/gps-track` instead.\n  For an indoor run's pace chart, `POST`/`GET /exercise-entries/:id/distance-series` (`attachDistanceSeriesToExerciseEntry`, `getExerciseEntryDistanceSeries`, table `exercise_entry_distance_series`, rows of `[active seconds, metres]`; `setExerciseEntryDistance` rescales them with the laps).\n- Correcting the distance of an entry with no GPS track (a treadmill run, from the summary screen):\n  inspect `POST /exercise-entries/:id/distance` in `routes/exerciseEntryRoutes.ts`, `services/exerciseEntryService.ts`'s `setExerciseEntryDistance` and `setExerciseEntryDistanceRequestSchema` in `shared/src/schemas/api/ExerciseEntries.api.zod.ts`. It changes only the entry's `distance`, its sets' distance and any laps (`distance_meters` and speed scaled by new over old distance), in one transaction, so measured heart rate and energy are kept; an entry with a GPS track is refused with 409.\n- Water, hydration, caffeine, or alcohol issue:\n  inspect `services/hydrationTotalsService.ts` (the single owner of the daily water formula), `services/measurementService.ts` (the container \"+/-\" path and the container->food link), `services/caffeineKineticsService.ts` / `services/alcoholWeekService.ts`, `models/waterContainerRepository.ts`, and the shared maths in `../shared/src/nutrients/`\n- Self-service \"delete synced data by source\" issue:\n  inspect `routes/syncedDataRoutes.ts`, `services/syncedDataService.ts`, and `models/syncedDataRepository.ts` (the `SYNCED_SOURCE_TABLES` whitelist)\n- AI chat or chatbot tool issue:\n  inspect `services/chatService.ts`, `ai/tools/`, and the matching domain service and repository\n- Fasting or mood issue:\n  inspect `routes/fastingRoutes.ts` / `routes/moodRoutes.ts` and `models/fastingRepository.ts` / `models/moodRepository.ts`\n  `mood_entries` holds several rows per day, each with `entry_timestamp` and a `source`: `manual` (web/app check-ins, AI `log_mood`, end-of-fast mood), or `garmin` / `import` (derived, one row per source per day via a partial unique index). `mood_value` is nullable; a CHECK needs at least one of mood, energy, focus, notes or tags (whitespace-only notes and blank tags do not count, as in `hasMoodContent`), and another limits `source` to those three values. Requests without the `X-Mood-Model-Version` header keep the one-mood-per-day contract on the \"legacy row\" (latest manual row with a mood, falling back to the derived row on GET, Garmin before import); `X-Mood-Model-Version: 2` always adds a check-in and reads arrays (header is in `CORS_ALLOWED_HEADERS`, `utils/corsHelper.ts`); its PUT edits only manual rows (409 for Garmin and import) and re-dates a check-in only when its timestamp actually changes. A check-in or derived mood written without a time of its own gets `instantForDay` from `@workspace/shared`: now for today, local noon for another day, so `entry_timestamp` stays on `entry_date`. Per-day aggregates (coach, cycle correlations) use `DAILY_MOOD_VALUE_SQL`: rounded average of manual moods, else Garmin, else import.\n- Symptom or episode tracking issue (#1882):\n  inspect `routes/v2/symptomRoutes.ts`, `services/symptomService.ts`, `models/symptomRepository.ts`, `models/symptomOptionRepository.ts`, and the shared contract and built-ins in `../shared/src/schemas/api/Symptoms.api.zod.ts` and `../shared/src/symptoms/`. `symptom_entries` also holds cycle-hub symptoms (`source = 'cycle'`), which RLS keeps owner-only\n- Medications, cycle, or pregnancy issue:\n  inspect the matching v2 route (`routes/v2/medicationRoutes.ts`, `routes/v2/cycleRoutes.ts`, `routes/v2/pregnancyRoutes.ts`), its Zod schema in `schemas/`, then `services/cycleService.ts` / `services/pregnancyService.ts` and the `models/medication*Repository.ts` / `models/cycleRepository.ts` / `models/pregnancyRepository.ts` files\n- Supplement barcode lookup:\n  `GET /api/v2/medications/supplement-lookup?upc=` (registered before `/:id`) runs only while the user can see an active `dsld` or `openfoodfacts` external provider (`getActiveProvidersByTypes`; neither is a 404; the NIH database answers first and `services/supplementOpenFoodFactsService.ts` asks Open Food Facts, per-serving nutrients only, when it has no match or `dsld` is off; migration `20261006200000_add_dsld_provider_type.sql`, constant `SUPPLEMENT_LOOKUP_PROVIDER_TYPE`) and calls `services/supplementLookupService.ts`. Two more routes read a Supplement Facts photo: `POST /supplement-label/scan` runs the user's vision AI (`services/supplementLabelScanService.ts`, same provider and error map as `/foods/scan-label`) and `POST /supplement-label/map` takes ingredients the phone read on device; both end in `mapScannedLabel`, so name matching and unit conversion are shared with the barcode lookup, which searches the NIH Dietary Supplement Label Database for the code, opens the hits to check the label's own UPC, and maps the label to one serving's nutrients in the app's units (IU for vitamins A, D and E converted, compounds and unplaceable ingredients returned as `unmatched`). The response contract is `shared/src/schemas/api/SupplementLookup.api.zod.ts`; an unreachable database is a 502, no match is `{ product: null }`.\n- Exercise alternatives, workout feedback, or adaptive suggestions issue (#1560):\n  inspect `services/exerciseAlternativesService.ts` (library + Free Exercise DB candidates, dedupe) with the pure ranking in `utils/exerciseAlternativesRanking.ts` and the muscle/equipment vocabulary in `../shared/src/constants/exerciseTaxonomy.ts`; feedback in `services/workoutCoachingService.ts` + `models/workoutFeedbackRepository.ts`; signals in `services/adaptiveWorkoutService.ts`. The rules that turn signals into weight changes are client-side and shared (`../shared/src/utils/adaptiveCoaching.ts`) so web, mobile and the AI tools agree; the server only reports what happened. AI actions: `suggest_alternatives`, `rate_workout`, `get_workout_coaching` in `ai/tools/exerciseTools.ts`\n- Bodyweight exercise load, volume or 1RM issue (#56):\n  a `bodyweight_reps` set's weight is signed added/assisting load, and its load is body weight plus that weight (`effectiveLoadKg` in `../shared/src/utils/exerciseLoad.ts`). SQL mirrors the rule in `utils/exerciseLoadSql.ts` (`bodyWeightJoinSql`, `setLoadSql`), used by `services/exerciseStatsService.ts` and `models/exerciseEntry.ts`'s progress query; `services/reportService.ts` applies the shared helper with readings from `reportRepository.getBodyWeightReadings`. Keep the two copies identical. A negative set weight is rejected unless that exercise's modality is `bodyweight_reps`. `models/exercise.ts`'s `createExercise` defaults the modality from equipment through `resolveExerciseModality`\n  `weight_distance` (weighted carries) and `weight_duration` (loaded holds) store weight plus distance (km) or duration (s); stats treat them as weight-based, not cardio, and best-set ordering falls back to distance then duration.\n- Sleep or sleep-science issue:\n  inspect `routes/sleepRoutes.ts`, `routes/sleepScienceRoutes.ts`, `services/sleepAnalyticsService.ts`, `services/sleepScienceService.ts`, and the sleep repositories\n\n## Architecture Resources\n\nBefore adding a feature or changing auth/permission behavior, read:\n\n- `../docs/src/developer/database.md` — Quick table index (all ~120 tables with purpose) + migration best practices\n- `../docs/src/developer/database-security-tiers.md` — Security tier, permission type, and RLS rules for every table (authoritative)\n- `../agent-docs/architecture-permissions.md` — Permission types, links to tier classification doc\n- `../agent-docs/data-flow-patterns.md` — Data flow from frontend through server to database, safe RLS patterns\n- `../agent-docs/new-domain-template.md` — Checklist for adding a major feature domain\n- `../agent-docs/anti-patterns.md` — Common mistakes (using getSystemClient(), forgetting RLS, cache invalidation, timezone bugs, cross-package contract mismatches)\n\n## Working Rules\n\n- Match the existing service/repository/middleware layering instead of introducing parallel abstractions\n- **Library Deletes vs Diary Snapshots:** `exercise_entries` and `food_entries` are snapshot-backed (`exercise_id` / `food_id` are `ON DELETE SET NULL`). `deleteExercise` and `deleteFood` (`mode: 'delete'`) must never delete past or today's diary entries; they cascade from templates/presets, clean up future scheduled plan entries (`entry_date >= today AND workout_plan_assignment_id IS NOT NULL`), and clean up empty parent preset entries. Only explicit `delete_with_history` (force delete) deletes diary entries for that user. If an item is referenced by others (`otherUserReferences > 0`), the delete must fall back to `hide` (`is_quick_exercise` / `is_quick_food`).\n- If your change adds a new domain, route family, or table, update this file's Snapshot, Source Map, and Quick Routing sections (and the `Last updated` date) in the same change\n- If you add persisted or user-visible data, think through migration, RLS, permissions, tests, API docs, and downstream client contracts together\n- Validate shared-contract","SparkyFitnessMobile/CLAUDE.md":"See @AGENTS.md\n","CLAUDE.md":"See @AGENTS.md\n","SparkyFitnessServer/CLAUDE.md":"# SparkyFitnessServer - Claude Agent Guidelines & System Prompt\n\nPath: `SparkyFitnessServer/CLAUDE.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/CodeWithCJ/SparkyFitness/HEAD/SparkyFitnessServer/CLAUDE.md)"},"items":[{"name":"AGENTS.md","path":"AGENTS.md","rawUrl":"https://raw.githubusercontent.com/CodeWithCJ/SparkyFitness/HEAD/AGENTS.md","title":"AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"# AGENTS.md\n\n*Last updated: 2026-10-22*\n\nThis is the repo-root monorepo guide for SparkyFitness. Use it to choose the right package, understand shared repo-level rules, and find the next guide to read.\n\n**For AI Tools & Developers:** Start with `agent-docs/README.md` (in this repo) for quick navigation to:\n- `file-and-domain-reference.md` — Find any code by feature in seconds\n- `testing-patterns.md` — Concrete test examples for each layer\n- `architecture-permissions.md` — Permission types and RLS patterns\n- Plus 5 more guides for migrations, data flow, anti-patterns, new features, and planning\n\nPackage-level guides win. For work inside a package, follow that package's `AGENTS.md` when present, otherwise its `CLAUDE.md`.\n\n## Scope\n\n- Start here when work begins at repo root or spans multiple packages.\n- Keep root-level guidance focused on workspace layout, shared conventions, and cross-package coordination.\n- Root `package.json` is tooling only (`husky`, `lint-staged`, `prettier`), not an app entrypoint.\n- Run scripts from the package directory you are changing.\n\n## Package Guides\n\n- Repo-root alias: `CLAUDE.md` points to this file.\n- Frontend: `SparkyFitnessFrontend/AGENTS.md`\n- Server: `SparkyFitnessServer/AGENTS.md`\n- Mobile: `SparkyFitnessMobile/AGENTS.md`\n- Shared: `shared/AGENTS.md`\n\nFor `docs/` and `SparkyFitnessGarmin/`, there is no package-level `AGENTS.md`. `SparkyFitnessGarmin/` is only a handful of Python files (`main.py`, `routes.py`, `service.py`, `schemas.py`); read them directly. For `docs/`, inspect the local manifest and content layout.\n\n## Monorepo Map\n\n- `SparkyFitnessFrontend/` - React 19 + Vite web app.\n- `SparkyFitnessServer/` - Express 5 + PostgreSQL backend API.\n- `SparkyFitnessMobile/` - Expo SDK 57 / React Native 0.86 app.\n- `shared/` - source-first TypeScript workspace package for `@workspace/shared` schemas, constants, and timezone/day helpers.\n- `docs/` - VitePress documentation site.\n- `docs/readme/` - translated copies of the root `README.md` (`README_<lang>.md`), outside the VitePress `srcDir`. The language bar at the top of `README.md` links to them; when the root README changes substantially, the translations go stale.\n- `SparkyFitnessGarmin/` - standalone Python integration service outside the current `pnpm` workspace.\n- `docker/`, `helm/`, `.github/` - infra and deployment assets.\n- `umbrel/sparkyfitness/` - Umbrel App Store package (manifest, compose, derived secrets), submitted to `getumbrel/umbrel-apps`. See `umbrel/README.md`.\n- `db_schema_backup.sql` - repo-root schema snapshot kept in sync by CI (`.github/workflows/schema-backup.yml`); never hand-edit or regenerate locally.\n- `docker/.env.example` - tracked env template commonly copied to repo-root `.env`.\n\n## Workspace Notes\n\n- `pnpm-workspace.yaml` currently lists `frontend`, `SparkyFitnessFrontend`, `shared`, `SparkyFitnessMobile`, `SparkyFitnessServer`, and `docs`.\n- Only `SparkyFitnessFrontend/` exists on disk right now; treat `frontend` as a legacy workspace entry unless the task is specifically about workspace cleanup.\n- `shared/` is a library package, not an app; its `pnpm run validate` runs typecheck, a hardcoded-string audit, and the Prettier check. CI runs that in a `Shared Validate` job and also re-runs the frontend, mobile, and server jobs whenever `shared/` changes, so validate shared changes from the consuming package(s), not in isolation.\n- `SparkyFitnessGarmin/` is outside the current workspace. Inspect its own manifest and scripts before working there.\n\n## Agent Efficiency (read this before searching)\n\nDo not read or search these paths; they burn context for nothing:\n\n- `WIP/` - personal scratch area; contains zips and full copies of other repos, including a stale duplicate of this repo (`WIP/SparkyFitness-main/`). Never read or edit anything under it.\n- `SparkyFitnessMobile/ios/` and `SparkyFitnessMobile/android/` - generated native projects (`ios/` is >1 GB of Pods). Regenerate with `npx expo prebuild --clean`; edit `app.config.ts`, `plugins/`, or `targets/` instead.\n- `pnpm-lock.yaml` (~1.3 MB) - never read; check `package.json` files instead.\n- `db_schema_backup.sql` (~330 KB) - never read whole; grep for the one `CREATE TABLE` you need.\n- `SparkyFitnessFrontend/dist/` - build output.\n- `SparkyFitnessFrontend/public/locales/` except `en/` - 35 machine-synced translations. Only `en/translation.json` is ever hand-edited, and even that (~120 KB) should be grepped, not read whole.\n\nCheap ways to learn things:\n\n- Database table index: read `docs/src/developer/database.md` (quick reference of all ~120 tables with one-line purpose). For detailed schema, read `shared/src/schemas/database/<Table>.zod.ts` (one small Zod file per table).\n- Database security & permissions: `docs/src/developer/database-security-tiers.md` (security tier, permission type, and RLS rules for every table).\n- API request/response contract: `shared/src/schemas/api/<Name>.api.zod.ts`.\n- Definition of done: CI (`.github/workflows/ci-tests.yml`) runs `pnpm run validate` (which includes Knip unused export & dead code checks in frontend and mobile) plus the package's CI test script for each changed package. Run those locally before declaring work complete.\n\n## Cross-Package Rules\n\n- If you add or change a server migration (such as creating a new table), follow `agent-docs/new-migration-checklist.md`. In short, you MUST:\n  1. Create the migration file in `SparkyFitnessServer/db/migrations/YYYYMMDDHHMMSS_description.sql`.\n  2. Update the Row-Level Security (RLS) policies in `SparkyFitnessServer/db/rls_policies.sql`.\n  3. **Restart the server** (`pnpm start` from `SparkyFitnessServer/`) to apply the migration.\n  4. Leave `db_schema_backup.sql` alone — after merge, CI regenerates it from the migrations and opens an automated sync PR (`.github/workflows/schema-backup.yml`). Never manually edit the backup file or commit a locally generated copy.\n  5. Add or update the matching Zod schema in `shared/src/schemas/database/`.\n  6. Update the user-facing documentation in `docs/src/features/family-friends-sharing.md`.\n  7. Update the developer documentation in `docs/src/developer/database-security-tiers.md` to classify the table as Tier 1, Tier 2, or Tier 3.\n- **Heart rate zones have one source of truth:** `shared/src/utils/heartRateZones.ts` (`resolveHeartRateZones`, `heartRateZoneRange`, `computeTimeInZones`). Never re-derive max heart rate or zone floors in a package; server stored zones, training load, mobile suggested ranges and the web/mobile charts and settings all call it. Max heart rate is the person's own value, else Nes (211 - 0.64 x age) from date of birth, else 190. Settings never rewrite stored `exercise_entry_hr_zones` rows. iOS 27 can fill custom floors from Apple Health (`floorsFromAppleHeartRateZones`); that does not change max heart rate. A finished Apple workout's own zone group (`bucketsFromAppleWorkoutZones`) is stored as `device` zones instead of recomputing, so the chart matches Apple, including time below the first boundary.\n- Prefer the shared timezone helpers from `@workspace/shared` and `SparkyFitnessServer/utils/timezoneLoader.ts` for day-string logic. Avoid `toISOString().split('T')[0]` for user-facing or business-logic dates.\n- Keep `YYYY-MM-DD` values as calendar-day strings until you reach a database or external API boundary that needs UTC instants.\n- Auth or API contract changes usually need a quick check in both web and mobile because they share the same backend.\n- Frontend local dev proxies `/api`, `/health-data`, and `/uploads` to the server on `3010`. The `/health-data` proxy is rewritten to `/api/health-data`, while server APIs remain rooted at `/api`.\n- Server runtime secrets are usually sourced from repo-root `.env`, commonly created from `docker/.env.example`. The server can also load secret files via `SparkyFitnessServer/utils/secretLoader.ts`.\n- Extract shared logic on the **second** duplication (\"rule of two\"), not the third - duplicated logic drifts as different sessions edit each copy. Extract *behavior*, not coincidental shape. See `agent-docs/anti-patterns.md`.\n- **Strict TypeScript Typing:** Never use `any` or `// eslint-disable-next-line @typescript-eslint/no-explicit-any` when creating new functions or editing existing code. When modifying existing files, proactively update legacy `any` declarations in the touched code to reflect their proper TypeScript data types, interfaces, or schemas from `@workspace/shared`. Do NOT copy legacy `any` parameter signatures forward when refactoring or extending service/repository files.\n- **Library Deletes vs Diary Snapshots:** `exercise_entries` and `food_entries` are self-contained snapshots, not pointers (`exercise_id` and `food_id` are `ON DELETE SET NULL`). Deleting an exercise or food from the library (`mode: 'delete'`) preserves past and current diary history, cascades from presets and plan templates, cleans up future scheduled workout plan entries (`entry_date >= today`), and cleans up empty parent preset entries. Only explicit `delete_with_history` (force delete) purges diary logs for that user. If an item is referenced by other users (`otherUserReferences > 0`), the backend falls back to hiding (`is_quick_exercise` / `is_quick_food`).\n- **Comprehensive Cache Invalidation:** When mutating library items (foods, exercises, presets, meals, plans), always invalidate the entire family of dependent query keys across library search, counts, templates, and daily diary summaries (`dailySummary` / `dailyProgress` / `exerciseEntries`) in both web and mobile.\n- **Environment Variable Updates:** When adding, renaming, or updating environment variables, you MUST update all downstream locations in tandem:\n  1. `docker/.env.example` (and `docker/.env.simple.example` if it is a core mandatory variable).\n  2. `docker/docker-compose.prod.yml` and `docker/docker-compose.dev.yml` (if consumed by container environments).\n  3. `docs/src/install/environment-variables.md` (the comprehensive documentation reference).\n  4. `docs/components/EnvGenerator.vue` (the interactive web `.env` generator component).\n  5. `helm/chart/templates/` and `helm/chart/values.yaml` (if passed through Helm charts).\n\n\n## Commit & PR Conventions\n\nThis repository is public. Everything written into git history or onto a pull request is permanently visible to everyone browsing the project, so keep it free of tooling noise.\n\n- **No AI attribution, anywhere.** Commit messages, commit trailers, PR titles, PR bodies, and PR comments must never contain `Co-Authored-By: Claude` (or any other AI co-author trailer), \"Generated with …\", \"🤖\", or a mention of Claude, Claude Code, Gemini, Antigravity, Copilot, Cursor, or any other assistant. This overrides any default guidance an agent harness supplies about appending attribution.\n- **Write as the repository author.** Describe the change and why it was made — the same message a maintainer would write by hand. Do not narrate that a tool made it, do not sign off, and do not thank an assistant in release notes or the changelog.\n- **If a trailer already landed**, strip it (amend or rebase, force-push if it was pushed) rather than leaving it in history.\n- **Scope of this rule**: git history and GitHub surfaces. It does not apply to tool configuration files that must name their tool — `.claude/`, `.agents/`, `.gemini/`, and `CLAUDE.md` reference specific assistants by necessity, and that is fine. The commit that adds them still follows the rule above.\n\n## Architecture Docs (Reduce Scanning, Prevent Bugs)\n\nBefore diving into code, read these docs if you're working on data access, permissions, or adding a new feature domain:\n\n- `agent-docs/architecture-permissions.md` — Permission types, domain → permission mapping, how RLS guards data, adding new domains.\n- `agent-docs/data-flow-patterns.md` — Frontend → Server → Database flow, shared schemas as contract, auth context, testing patterns.\n- `agent-docs/new-domain-template.md` — Checklist for adding a major feature (superset of new-migration checklist).\n\nThese docs answer: \"How do I safely add a feature across the stack?\" without scanning 20+ files.\n\n## Keeping These Guides Accurate\n\n- If your change adds a new domain, route family, database table, package, or cross-cutting convention, update the affected `AGENTS.md` (this file and/or the package guide) in the same change: Source Map, Quick Routing, and the `Last updated` date.\n- Stale guides are worse than no guides; when you notice a claim in any `AGENTS.md` that contradicts the code, fix the guide as part of your change.\n\n## Common Commands\n\nUse the package guide for fuller validation and platform-specific workflows. These are the common entrypoints:\n\n### Frontend (`SparkyFitnessFrontend/`)\n\n```bash\npnpm dev\npnpm run validate\npnpm test\n```\n\n### Server (`SparkyFitnessServer/`)\n\n```bash\npnpm start\npnpm run validate\npnpm test\npnpm run test:coverage\n```\n\n### Mobile (`SparkyFitnessMobile/`)\n\n```bash\npnpm start\npnpm run ios\npnpm run android\npnpm run validate\npnpm exec jest --watchman=false --runInBand\nnpx expo prebuild --clean\n```\n\n### Docs (`docs/`)\n\n```bash\npnpm dev\npnpm run build\n```\n\n","isInternal":false,"tokens":3148,"sizeBytes":13199},{"name":"AGENTS.md","path":"shared/AGENTS.md","rawUrl":"https://raw.githubusercontent.com/CodeWithCJ/SparkyFitness/HEAD/shared/AGENTS.md","title":"shared - AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"# AGENTS.md\n\n_Last updated: 2026-10-22_\n\n`@workspace/shared` is a source-first TypeScript workspace library package for schemas, constants, and timezone/day helpers consumed by SparkyFitnessServer, SparkyFitnessFrontend, and SparkyFitnessMobile.\n\n## Scope\n\n- This package defines contracts and shared logic, not an app.\n- Validate changes from consuming packages (server, frontend, mobile), not in isolation.\n- Every schema change here potentially touches three packages.\n\n## Food Assistant State\n\n`schemas/api/FoodAssistant.api.zod.ts` and `schemas/database/FoodAssistant.zod.ts` define lasting preferences, versioned tasks, unresolved ingredients and idempotent operation records. All state is owner-only; these additive contracts do not change existing chat clients.\n\n## Structure\n\n- `src/schemas/database/` - one Zod file per table (`Foods.zod.ts`, `Exercises.zod.ts`, ~115 files). Agent shortcut: to learn a table shape, read the matching file here instead of the SQL dump.\n- `src/schemas/api/` - API request/response contracts (`*api.zod.ts`). `VoiceTranscription.api.zod.ts` defines multipart limits/MIME types, transcription responses, voice preference patches, and compatible public service options shared by web/mobile/server.\n- `src/constants/` - shared constants and enums (exercises, nutrients, meal types, fasting protocols, medication schedules, cycle phases, etc.).\n- `src/utils/` - timezone helpers (`todayInZone`, `instantToDay`, `instantForDay`, `dayToUtcRange`, `compareDays`, `addDays`, `isDayString`), cycle/menstruation helpers, unit/calculation utilities, the race plan generator (`racePlan.ts`, `buildRacePlan`), the run program schedule helpers (`runProgramSchedule.ts`, `runProgramCatchUp.ts`, `runProgramTimeline.ts`: `buildProgramTimeline` / `itemsByDay`, and `runProgramActions.ts`: the `PUT /api/v2/run-program` body builders; web and mobile both use them), and the training pace zones (`racePaceZones.ts`: `racePaceZones` turns a recent race result into easy, interval, tempo, marathon-pace and stride ranges with Riegel's formula; `equivalent5kPace` is the shared 5K conversion; `paceZones.ts` holds the effort-based `paceZones`, `thresholdPaceFromEffort` and `PACE_ZONE_IDS`), and the plan realigner (`runProgramRealign.ts` (`realignProgram` rebuilds the rest of a plan to end on the goal day: drops repeats and the shortest easy runs first, folds lighter weeks, never touches the taper or final workout, keeps two runs a week for three-plus run days, stretches with repeated base weeks when there is room, eases after a long gap, and reports an unrealistic goal with the options; `applyRealign` is the confirmed change, `toRealignResult` the API shape); its contract is in `RunProgram.api.zod.ts`: `realignRunProgramBodySchema`, `runProgramRealignResultSchema`).\n- `src/utils/heartRateZones.ts` - the single source of truth for heart rate zones: `resolveMaxHr` (own value, else Nes from date of birth, else observed max, else 190), `resolveHeartRateZones` (three methods: `percent_max`, `heart_rate_reserve` (Karvonen, needs resting HR) and `custom` floors; falls back to percent of max when the chosen method lacks input and says so in `effectiveMethod`), `heartRateZoneRange(resolved, zone)` (bpm range of zone 1-5; the top zone ends at max HR - the function for training programs that need \"zone N in bpm\"), `computeTimeInZones` (buckets a heart rate series), `percentMaxFloors` / `heartRateReserveFloors`, `floorsFromAppleHeartRateZones` (five custom floors from Apple's preferred zones; null unless there are exactly five, and a missing zone-1 minimum becomes 30 bpm), `bucketsFromAppleWorkoutZones` (a finished workout's five zone durations, keeping time below the first boundary as zone 1; empty when Apple reported no time, null when the group is not five ascending zones). `trainingLoad.ts` weights sessions by these zones. The API contract is `src/schemas/api/HeartRateZones.api.zod.ts`.\n- `src/nutrients/` - nutrient catalogs and the goal calculator used by web onboarding, the web Goals editor and mobile Goals (`nutrientCalculation.ts`: per-nutrient recommended values from age, sex, weight, calories and the saved algorithm choices; `nutrientAlgorithms.ts`: the algorithm enums and labels).\n- `src/ai/`, `src/cycle/`, `src/medications/`, `src/mood/` - domain-specific helpers. `src/medications/adherence.ts` is the one implementation of medication adherence for web and mobile (`computeAdherence`, `computeChecklistProgress`, `entryMatchesDue`, and the month-calendar helpers `tallyEntriesByDay`, `dayLogStatus`, `tallySymptomsByDay`); `subtype.ts` holds the All | Meds | Supplements filter rules. Both are pure, take timezone and data as arguments, and are tested in `SparkyFitnessServer/tests/medicationAdherence.test.ts` and `medicationSubtype.test.ts`.\n- `src/types/` - shared plain TypeScript types that are not Zod-derived.\n- Routes: `src/schemas/api/Route.api.zod.ts` (points, turn cues, bodies), `src/utils/routeGeometry.ts` (distance, climb, simplify, bounds, and the `localProjector` / `distanceToSegment` nearest-point helpers the route editor uses), `src/utils/routeGpx.ts` (dependency-free GPX read/write), `src/utils/routeGenerate.ts` (generate and follow-roads request builders, error kinds, turn counts) and `src/constants/routingProviderTypes.ts` (the `valhalla` external provider type and its default public server URL).\n- Routes: `src/schemas/api/Route.api.zod.ts` (points, turn cues, bodies), `src/utils/routeGeometry.ts` (distance, climb, simplify, bounds) and `src/utils/routeGpx.ts` (dependency-free GPX read/write).\n- `src/compare/` - compare any two tracked things: `stats.ts` (statistics), `metrics.ts` (metric catalog: kind, missing-day rule, weekly form, and `rolling` for a value read over the 7 days ending on the day), `analyze.ts` (day and week-by-week analysis), `since.ts` (before vs. since a habit started: suggested start, stretches, regularity, pre-trend; its limits are exported as `SINCE_*` constants), `question.ts` (the question model web and mobile share: timing choices, spans and `compareWindow`, custom-range limits, since-start bounds, `SUGGESTED_QUESTIONS`, and the recents helpers `knownCompareQuestions` / `withRecentQuestion`), `chartData.ts` (chart data both apps draw: `pairedDots` and `displayDots`, `trendLine`, `overTimeData`, `sinceChartData`). The API contract is `src/schemas/api/Compare.api.zod.ts`.\n- `src/symptoms/` - generic symptom tracking: constants and enums (scales, templates, sections, option kinds), template section resolution (`resolveSections`), built-in symptoms and pick-lists, head/body region ids for the location maps, and custom-field validation. The API contract is `src/schemas/api/Symptoms.api.zod.ts`.\n\n## Naming Convention\n\n- `X.api.zod.ts` = API request/response schema\n- `X.zod.ts` = database table schema\n- Export everything from `src/index.ts`; consuming packages import both types and values via `@workspace/shared`\n\n## Cross-Package Contract Rules\n\n- Changes to `src/schemas/api/` usually affect server routes and both frontend/mobile API clients.\n- Changes to `src/schemas/database/` require matching server migrations and RLS policies when changing the stored schema. CI regenerates the schema backup after merge; never hand-edit or regenerate it locally. Mirroring existing columns missing from a Zod schema does not require another migration.\n- Timezone/day-string helpers prevent bugs; prefer them over `toISOString().split('T')[0]`.\n- Verified commands (run from `shared/`): `pnpm run typecheck`, `pnpm run format:check`, `pnpm run format`, `pnpm run i18n:audit`, `pnpm run validate` (typecheck + i18n audit + format check). The package has no lint or tests of its own; behavior is covered by the consumers' test suites.\n- Shared exports ids and data; apps own translation. `pnpm run i18n:audit` (`scripts/audit-user-facing-strings.mjs`) fails when a file gains more prose in `label`/`title`/`description`/`message`/`hint`/`summary`/`body`/`text` properties than `scripts/user-facing-strings.baseline.json` allows (the cycle, GLP-1, and body-region English catalogs are grandfathered). After moving text out, run it with `--update-baseline` to lower the allowance; use `// i18n-ignore` for literals that must stay.\n- Formatting uses the same `.prettierrc` as the other packages, and lint-staged formats staged `shared/` files on commit.\n- CI (`.github/workflows/ci-tests.yml`) runs `Shared Validate` and re-runs the frontend, mobile, and server jobs on any `shared/**` change.\n- Test any shared change from the consumer packages (`pnpm run validate` in SparkyFitnessServer, SparkyFitnessFrontend, and SparkyFitnessMobile after modifying shared).\n\n## Working Rules\n\n- Keep this package export-focused and schema-focused; logic that scales should live in consuming packages.\n- Never export stale or unfinished types; if a consumer is drafting code and needs a type not yet here, add it.\n","isInternal":false,"tokens":2189,"sizeBytes":8930},{"name":"AGENTS.md","path":"SparkyFitnessFrontend/AGENTS.md","rawUrl":"https://raw.githubusercontent.com/CodeWithCJ/SparkyFitness/HEAD/SparkyFitnessFrontend/AGENTS.md","title":"SparkyFitnessFrontend - AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"# AGENTS.md\n\n_Last updated: 2026-10-23_\n\nSparkyFitness Frontend is the React web app for the SparkyFitness monorepo. Use this file as the primary guide for work inside `SparkyFitnessFrontend/`.\n\nIf a task also touches the server, mobile app, or `shared/`, read that package guide before editing outside this directory. Use `../AGENTS.md` for monorepo-level context.\n\n## Scope\n\n- This file is for package-local work in `SparkyFitnessFrontend/`.\n- `CLAUDE.md` just imports this file via `See @AGENTS.md`.\n- Run scripts from this directory.\n\n## Current Snapshot\n\n- Stack: React 19, Vite 8, TypeScript 6, Tailwind CSS v4 (via `@tailwindcss/vite`), shadcn/ui-style Radix primitives, TanStack Query 5, React Router 7 (`createBrowserRouter`), i18next, Better Auth client, Zod 4, Recharts.\n- `@/*` maps to `src/`; `@workspace/shared` maps to `../shared/src/index.ts` (also in Jest via `moduleNameMapper`).\n- Dev server runs on port `8080` and proxies `/api`, `/mcp`, and `/uploads` to the backend on `3010`; `/health-data` is proxied with an `/api` prefix rewrite. Override the backend host with `VITE_BACKEND_HOST`.\n- PWA (`vite-plugin-pwa`) is enabled in production builds only.\n\n## Verified Commands\n\n```bash\npnpm dev\npnpm run typecheck\npnpm run lint\npnpm run format:check\npnpm run format\npnpm run validate\npnpm test\npnpm run test:ci\npnpm run build\n```\n\n- `pnpm run validate` runs typecheck, lint (`--max-warnings 0`), Prettier check, and Knip (`pnpm run knip` for unused files and exports) together.\n- `pnpm test` runs Jest (`ts-jest`, `jsdom`); config is inline in `package.json`, setup in `src/tests/setupTests.ts`.\n- `pnpm run build` runs `validate` first, then `vite build`.\n- CI (`.github/workflows/ci-tests.yml`) runs `pnpm run validate` and `pnpm run test:ci` for this package when its files change; matching those locally means a green PR.\n\n## Domain-Mirrored Layout (the most important convention)\n\nFeatures are organized by domain, and the same domain folder name appears in `src/pages/`, `src/api/`, and `src/hooks/`. A feature change usually touches the matching folder in all three:\n\n- Page domains: `Admin`, `Auth`, `Chat`, `CheckIn`, `Cycle`, `Diary`, `Errors`, `Exercises`, `Fasting`, `Foods`, `Goals`, `Integrations`, `Medications`, `Reports`, `Settings`, `Symptoms`.\n- API domains add a few more: `AiConversions`, `Chatbot`, `Onboarding`, `Pregnancy`, `SleepScience`, `Symptoms`.\n- Example: a Medications bug lives in `src/pages/Medications/` + `src/api/Medications/` + `src/hooks/` medication hooks. Start there, not with a repo-wide search.\n\n## Source Map\n\n- Food assistant state: owner-only preferences, recoverable tasks and operation history use `FoodAssistant.api.zod.ts` / `FoodAssistant.zod.ts` in shared. Server: `routes/v2/foodAssistantRoutes.ts`, `services/foodAssistantService.ts`, `models/foodAssistantRepository.ts`, `ai/tools/foodAssistantTools.ts`. Web: `src/components/ai/FoodAssistantState.tsx`, `src/api/Chatbot/foodAssistantService.ts`.\n\n- `src/main.tsx` - app bootstrap; creates the shared `QueryClient` with global `QueryCache`/`MutationCache` handlers that render toasts from query/mutation `meta` (`errorTitle`, `errorMessage`, `successMessage`).\n- `src/App.tsx` - route registry via `createBrowserRouter`, plus `PrivateRoute` and `PermissionRoute` wrappers (permission-gated areas include `reports` and `admin`).\n- `src/pages/<Domain>/` - route screens by domain.\n- `src/pages/Reports/Compare/` - the Reports > Compare tab (`CompareReport` is the root, mounted by `Reports.tsx` outside its loading gate), with its data from `src/hooks/Reports/useCompare.ts` and `src/api/Reports/compareApi.ts`.\n- `src/api/api.ts` - `apiCall(endpoint, options)` helper: base URL `/api`, query `params`, JSON/FormData bodies, `responseType`, error toasts, `suppress404Toast`. Use it for all backend requests.\n- `src/api/<Domain>/` - per-domain API clients built on `apiCall`.\n- `src/hooks/<Domain>/` and `src/hooks/use*.ts(x)` - TanStack Query hooks and shared UI hooks (`use-toast`, `useDebounce`, `useAuth`, ...).\n- `src/components/` - shared components; `ui/` holds the shadcn-style primitives (~37 files); domain component folders include `Foods/`, `FoodSearch/`, `FoodUnitSelector/`, `Onboarding/`, `ExerciseCharts/`, `ai/` (assistant-ui chat pieces).\n- `src/contexts/` - `ActiveUserContext` (family-access acting-user switching), `PreferencesContext`, `ThemeContext`, `WaterContainerContext`, `ChatbotVisibilityContext`, `ChatToolCategoriesContext` (runtime chat tool-category selection, localStorage-backed).\n- `src/layouts/` - `MainLayout.tsx` and `AddComp.tsx`.\n- `src/lib/` - `auth-client.ts` (Better Auth React client), `utils.ts` (`cn`), scanner engines, sleep helpers.\n- `src/services/` - pure calculation helpers (BMR, body composition, nutrient calculation), not HTTP clients.\n- `src/utils/` - logging, user preferences, date helpers, misc. `browserVoiceInput.ts` owns browser speech/recording lifecycle; `hooks/AI/useVoiceInput.ts` appends transcripts to drafts without sending.\n- `src/tests/` - Jest suites mirroring `components`/`contexts`/`hooks`/`services`/`utils`, plus `test-utils.tsx`.\n- `public/locales/<lng>/translation.json` - i18next resources, loaded over HTTP at runtime.\n\nWhen searching, ignore `node_modules/`, `dist/`, and every locale except `public/locales/en/`.\n\n## Translations (i18n)\n\n- Only ever edit `public/locales/en/translation.json`. The other 35 locales are machine-synced through the `sync-translations.yml` workflow and a separate SparkyFitnessTranslations repo; hand-editing them creates conflicts with that pipeline.\n- UI strings go through `useTranslation()` / `t('...')` keys, not hardcoded literals.\n- `en/translation.json` is ~120 KB - grep for the key or section you need instead of reading the whole file.\n- Developer docs: `../docs/src/developer/translations.md`.\n\n## Conventions\n\n- Use `apiCall` (or an existing per-domain client) for backend requests; don't hand-roll `fetch`.\n- Prefer declaring toast text via React Query `meta` on the query/mutation instead of imperative `toast(...)` calls where the global handlers cover it.\n- Use `src/utils/logging.ts` helpers instead of bare `console.*`; verbosity follows the user's logging-level preference.\n- Keep `YYYY-MM-DD` values as calendar-day strings; use the shared timezone/day helpers from `@workspace/shared` instead of `toISOString().split('T')[0]`.\n- To learn a database table's shape, read `../shared/src/schemas/database/<Table>.zod.ts` - do not read `../db_schema_backup.sql` or the migrations.\n- Auth flows go through `src/lib/auth-client.ts` and `useAuth`; acting-user (family access) state lives in `ActiveUserContext` and affects most data hooks.\n- New UI should reuse `src/components/ui/` primitives and existing shared components before adding new ones.\n- Food and meal quantity fields use `NumericInput` with `allowExpressions` for basic arithmetic. `utils/evaluateArithmetic.ts` parses numbers, parentheses, and `+ - * /` without executing code; invalid expressions block form submission. Other numeric fields retain their existing behavior.\n- Time entry uses `TimeInput` (`'HH:mm'`) and `DateTimeInput` (`'YYYY-MM-DDTHH:mm'`) from `src/components/ui/TimeInput.tsx`, never native `<input type=\"time\">` or `type=\"datetime-local\"`: browsers pick 12h/24h from their language and ignore the user's `timeFormat` preference. Both take the native string formats, so call sites keep their state as-is; name them with `aria-labelledby` (an external `<Label htmlFor>` cannot target a multi-part field).\n- **Cache Invalidation on Library Mutations:** When mutating foods, exercises, presets, or plans, use the domain invalidation hooks from `src/hooks/useInvalidateKeys.ts` (`useExerciseInvalidation`, `useFoodInvalidation`, `useMealInvalidation`, `useDiaryInvalidation`) to invalidate the entire family of dependent query keys including search, presets, templates, and diary daily progress.\n- **Library Deletes & Snapshots:** Deleting foods or exercises uses `mode: 'delete'` which preserves logged diary history (via snapshots) and drops items from presets/plans; only explicit `delete_with_history` deletes diary entries. Empty presets are guarded against starting/logging.\n\n## Testing and Validation\n\n- Test files live in `src/tests/`, mirroring the source area they cover; use `test-utils.tsx` for rendering with providers.\n- Run the tests nearest the touched surface first, then `pnpm run validate` for cross-cutting changes.\n- Lint is strict (`--max-warnings 0`); unused imports fail the build.\n\n## Quick Routing\n\n- Routing/navigation/permission issue: `src/App.tsx` (router, `PrivateRoute`, `PermissionRoute`) and `src/layouts/MainLayout.tsx`.\n- API/error-toast issue: `src/api/api.ts`, then the domain client in `src/api/<Domain>/`, then the query/mutation `meta` in the calling hook.\n- Auth/session issue: `src/lib/auth-client.ts`, `src/hooks/useAuth.tsx`, `src/pages/Auth/`, and the server's `auth.ts` if it crosses packages.\n- Family-access/acting-user issue: `src/contexts/ActiveUserContext.tsx` and the hooks consuming it.\n- Chat (Sparky) issue: `src/pages/Chat/`, `src/components/ai/`, `src/api/Chatbot/`. Voice controls live in `VoiceInputControl.tsx`; `VoiceInputSettings.tsx` shares server-backed enable/System/AI choices with mobile. `active_voice_ai_service_id: null` means System, never automatic inheritance from Chat. A compatible Chat model is only a suggestion. Cancelling, leaving the page, hiding the tab, or changing voice preferences releases the microphone and aborts uploads.\n- Theme/preferences issue: `src/contexts/ThemeContext.tsx`, `src/contexts/PreferencesContext.tsx`, `src/api/Settings/preferences.ts`, `src/utils/userPreferences.ts`.\n- Calorie goal settings: `src/pages/Settings/CalculationSettings.tsx`; `components/WeeklyWeightGoalInput.tsx` edits the `weekly` goal mode with direction and kg, g, lbs or stones + pounds per week, defaulting to the profile weight unit. Store signed kilograms in `goal_mode_weekly_change_kg` (negative loss, positive gain, zero maintenance), preserving legacy percentage modes. Use shared `formatWeeklyGoalRate` / `parseWeeklyGoalRate` for display and input; switching units preserves the canonical rate. Pass `weeklyChangeKg` to `computeCalorieTarget`; Diary preview and `CalorieTargetBreakdown` use the same preference.\n- Missing/wrong UI text: the i18n key in `public/locales/en/translation.json` and the `t('...')` call site.\n- Exercise alternatives / workout feedback / adaptive suggestions (#1560): `AddExerciseDialog`'s `replaceFor` prop adds the **Suggested** tab (`pages/Exercises/ExerciseAlternativesPanel.tsx`); the workout player's Replace (lazy-loaded dialog) and load pass (`WorkoutPlaybackPage.tsx`, helpers in `utils/workoutPlayback.ts`) apply the shared `decideAdaptiveAdjustment` rules and keep both the usual and adapted sets on the draft for \"Use my usual\"; feedback is `pages/Diary/WorkoutFeedbackPanel.tsx` (finish dialog + expanded diary workout) via `hooks/Exercises/useWorkoutCoaching.ts`.\n- Medication adherence, streak, checklist progress and month-calendar status (`pages/Medications/TodayMedications.tsx`, `MedicationLogCalendar.tsx`) are computed by `@workspace/shared` (`medications/adherence.ts`), the same code the mobile adherence screen uses; change the rules there, not in the page. The meds/supplements filter rules (`filterMedsBySubtype`, `filterEntriesBySubtype`, `visibleDoseCards`) come from `medications/subtype.ts`.\n- Symptom or episode tracking (#1882): `src/pages/Symptoms/` (`SymptomsHub` is the tracker, shown on Check-in through `SymptomsPanel` and on Medications with `variant=\"medications\"`; `SymptomLogForm` is template-driven, `SymptomsReport` is Reports > Symptoms), `src/api/Symptoms/symptomService.ts`, `src/hooks/useSymptoms.ts`. Which sections a symptom asks about comes from `resolveSections` in `@workspace/shared`; family delegates with only the symptoms permission use the standalone `/symptoms` page.\n- Bodyweight exercises (#56): the `bodyweight_reps` layout in `constants/exercises.ts` (`signedWeight`) drives the +/− weight header and placeholder in the set editor; reports and trend charts count body weight through the shared `effectiveLoadKg` / `setVolumeKg` (`pages/Reports/ReportsTables.tsx`, `utils/exerciseTrendUtils.ts`, which reads `body_weight_kg` from the progress endpoint).\n- `weight_distance` / `weight_duration` (#127) have their own `SET_TABLE_LAYOUT` entries: carries swap the reps cell for a distance cell (stored km, edited in metres/yards via `carryDistanceFromKm` / `carryDistanceToKm` in `@workspace/shared`), loaded holds drop reps and use the duration column.\n- Training consistency (#59): `pages/Reports/TrainingConsistencyCard.tsx` in the Exercise report's left column, fed by `useTrainingConsistency` (`hooks/Reports/useReports.ts`) from `GET /reports/training-consistency`. A fixed 26-week window computed on the server by the shared `buildTrainingConsistency`, so it ignores the report's date filter; the existing `WorkoutHeatmap` stays the calendar.\n- Compare tab (day and week questions): `src/pages/Reports/Compare/CompareReport.tsx` holds the question and span state and wires the sentence, charts and tables; wording is `compareCopy.ts` and table rows are `compareTables.ts`; question, window and display-scale logic is `shared/src/compare/`. Since-habit questions are not offered on the web yet. It shows only the signed-in user's own data (no request while acting on behalf), and its range is its own, so the Reports date picker is hidden on this tab.\n- Shoe inventory: the Equipment section of the Exercises page (`src/pages/Exercises/EquipmentManager.tsx` + `ShoeFormDialog`; there is no separate tab or route), `src/components/Shoes/ShoeSelect.tsx` (the picker used by `pages/Diary/EditExerciseEntryDialog.tsx` for cardio entries), `src/api/Shoes/shoeService.ts`, `src/hooks/Shoes/useShoes.ts`. Distances are stored in km and edited in the user's unit; assigning a shoe invalidates both `shoeKeys` and `exerciseEntryKeys` because mileage is derived from linked entries. The Equipment section also shows while acting on behalf of someone: `can_manage_diary` delegates can edit it and `can_view_reports` delegates read it, as the server enforces.\n- Training programs (run and walk plans): the `/exercises/training-programs` route (`src/pages/Exercises/TrainingPrograms/`: `TrainingPrograms` is the page, `TrainingProgramPicker` the built-in list with the 3 or 4 runs a week choice and the person's own plans, `StartProgramDialog` the start day, `TrainingProgramCard` the running program, `TrainingProgramCalendar` the month view, projected finish day, goal-date fit and catch-up), reached from `TrainingProgramsEntry` on the Exercises page, and `pages/Diary/RunProgramBanner.tsx` in the Diary's exercise card. Data is `GET/PUT /api/v2/run-program` plus `POST .../complete` and `GET .../custom` through `src/api/Exercises/runPrograms.ts` and `src/hooks/Exercises/useRunProgram.ts`; the server route is owner-only (`requireSelfActor`), so every hook and the banner stay off while `isActingOnBehalf`. All schedule maths comes from `@workspace/shared` (`projectRuns`, `projectedFinishDay`, `programRunForDate`, `catchUpFor`, `buildProgramTimeline`), never re-derived here. Request bodies come from `utils/runPrograms.ts`; `useSaveRunProgramMutation` runs changes one at a time (mutation `scope`) and accepts a function of the program as it stands, so two quick weekday taps both land. Mutations write the response into `runProgramKeys.current()` and call `useRunProgramInvalidation` (program, workout plan, exercise entry and daily progress families); `useDiaryInvalidation` and `useExerciseInvalidation` also refresh `runProgramKeys.all`. English program names, descriptions and week layouts live in `constants/runProgramCopy.ts` and mirror `trainingPrograms.programs.<id>.*` in the locale file. The web does not record runs: **Mark workout as done** (card and Diary) is how a workout is ticked off. Not on web yet: creating or editing custom plans, the race plan builder, the effort rating, the ease-back offers and the workout detail screen.\n- Heart rate zones: Settings → Profile & Account → Heart Rate Zones (`src/pages/Settings/HeartRateZoneSettings.tsx`, `src/hooks/Settings/useHeartRateZones.ts`, `src/api/Settings/heartRateZones.ts` over `GET`/`PUT /api/v2/heart-rate-zones`). Zone maths is shared (`@workspace/shared` `heartRateZones.ts`); labels are in `src/utils/heartRateZoneLabels.ts`. `ActivityHeartRateZonesChart` takes `zoneMethod` (from the stored `zone_method` of the workout's zone rows) and says which method the zones were worked out with; stored zones are never recomputed when settings change.\n- Routing provider (Valhalla, route generation and Follow roads): a Settings > External Providers type like Mealie, with `pages/Settings/RoutingProviderFields.tsx` (empty server URL with an example placeholder and no default server, optional API key, privacy and public-server notes) used by `ProviderSpecificFields` (add) and `EditProviderForm`; the type row is seeded by the server migration, the server resolves it per person (see the server guide).\n- Chart issue: Recharts usage in the domain page plus `src/components/ExerciseCharts/` or `ZoomableChart.tsx`.\n\n## Priority Rule\n\n- For work inside `SparkyFitnessFrontend/`, this file wins over repo-root guidance on package-specific details.\n- If a task spans packages, combine this guide with the other affected package guides.\n- If you add a new domain folder, route family, or cross-cutting convention, update the Domain list, Source Map, and Quick Routing sections of this file in the same change.\n","isInternal":false,"tokens":4346,"sizeBytes":17684},{"name":"AGENTS.md","path":"SparkyFitnessMobile/AGENTS.md","rawUrl":"https://raw.githubusercontent.com/CodeWithCJ/SparkyFitness/HEAD/SparkyFitnessMobile/AGENTS.md","title":"SparkyFitnessMobile - AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"# AGENTS.md\n\n_Last updated: 2026-10-23_\n\nSparkyFitness Mobile is a React Native 0.86 + Expo SDK 57 app for syncing Apple Health / Health Connect data with the SparkyFitness backend, tracking nutrition, hydration, fasting, measurements, exercise, saved foods, meal templates, custom exercises, workout presets, iOS / Android widgets, the active workout HUD, and the Sparky AI chat.\n\nThis is the package guide for `SparkyFitnessMobile/`. Work from this directory for mobile implementation and validation. If a task crosses into the backend, frontend, or `shared/`, read that package guide too before editing outside mobile.\n\n## Scope And Style\n\n- TypeScript is strict. Keep changes type-safe and compiling cleanly.\n- Prefer small, direct changes that fit the existing screen, hook, and service boundaries.\n- For ambiguous bugs, prove which layer is failing before patching. One narrow diagnostic check beats speculative edits across multiple layers.\n- Do not replace a working implementation with a rewrite unless the requester explicitly approves that direction.\n- When asked to plan work, confirm scope with clarifying questions before exploring code or drafting the plan.\n- Run scripts from `SparkyFitnessMobile/`, except root package operations such as `pnpm install` for patched dependencies.\n- Treat `android/` and `ios/` as generated output when possible. Edit `app.config.ts`, `plugins/`, `targets/`, JS/TS sources, or patch files first, then regenerate with prebuild when needed.\n\n## Stack And Imports\n\n- Primary stack: React 19.2, React Native 0.86, Expo SDK 57, TypeScript 6, React Navigation 7, TanStack Query 5, Uniwind / TailwindCSS v4, Reanimated 4, Skia, Victory Native, Expo Background Task / Task Manager / Notifications, Zustand, assistant-ui + AI SDK (chat).\n- `@/*` maps to this package and `@workspace/shared` maps to `../shared/src/index.ts`.\n- Prefer `@workspace/shared` schemas, constants, date/timezone helpers, and types over local duplicates.\n- The app talks to the backend under `/api`; health uploads go to `POST /api/health-data`.\n- Global `fetch` is Expo's WinterCG `expo/fetch`, so React Native's `{uri, name, type}` FormData file parts throw \"Unsupported FormDataPart implementation\". Append an `expo-file-system` `File` (it implements Blob) for multipart uploads; see `pregnancyPhotosApi.ts`.\n- Server-stored distance/weight units are metric. UI conversion belongs in mobile helpers such as `unitConversions.ts`.\n\n## Localization contract\n\nEnglish (`en`) is the canonical source locale and the deterministic fallback. Feature developers must add/update the English catalog, use semantic static keys, provide explicit English fallback/defaultValue text, use count-based i18next pluralization, use app-locale date/number/unit formatters, and avoid user-facing hardcoded text. Custom, user, and server content stays literal.\n\nFeature developers do not need to know or translate Polish or any future language, and do not need to wait for Weblate. Translators/Weblate own Polish and future translations and linguistic QA. Missing translation content is non-blocking and falls back to English; existing translated content remains structurally validated.\n\nThe shipped locale registry is `src/localization/localeRegistry.json`, read through the typed accessors in `src/localization/localeRegistry.ts`. Adding a catalog to Weblate does not ship it. Shipping requires explicit registry enablement plus native/platform support verification. RN catalogs and native resources are separate surfaces (Expo metadata, Android widget resources, and iOS widget/Live Activity `.lproj` resources), each its own Weblate component and each synced by `.github/workflows/sync-translations.yml`. Only the `en` side of any surface is edited by hand; the i18n audit blocks on registered locales and reports unregistered ones without failing. The workflow pulls the widget resources for registered locales only, because Android and iOS compile whatever resource directories are present and there is no build-time registry check for them.\n\n## Commands\n\n```bash\npnpm start\npnpm run ios\npnpm run android\npnpm run lint\npnpm run typecheck\npnpm run validate\npnpm exec jest --watchman=false --runInBand\npnpm exec jest --watchman=false --runInBand <test-path>\npnpm run test:coverage -- --watchman=false --runInBand\nnpx expo prebuild --clean\n```\n\n- `pnpm run validate` runs i18n and muscle-figure generation checks, typecheck, lint, i18n audit, watch i18n audit, Knip (`pnpm run knip` for unused files and exports), native locales check, and Prettier check together.\n- Use Watchman-disabled Jest commands in agent/sandbox runs; bare Jest often fails on macOS.\n- `collectCoverage` is enabled in Jest config, so expect coverage output from normal test runs.\n- Run `npx expo prebuild --clean` after native dependency changes, permissions, app group or widget target changes, Expo plugin changes, native config edits, or patching native modules.\n- After editing the root `patches/react-native-health-connect@4.1.3.patch`, run `pnpm install` from the repo root, then prebuild from mobile.\n\n## App Shell And Navigation\n\n- `App.tsx` is the root composition point. `App()` wraps `QueryClientProvider`, `KeyboardProvider`, `GestureHandlerRootView`, and `BottomSheetModalProvider`; `AppContent()` owns `NavigationContainer`, `SafeAreaProvider`, navigators, `AddSheet`, auth modals, the embedded/floating active-workout bars, the tab-bar `WhatsNewBanner`, and toasts.\n- App-shell logic lives in dedicated hooks that `AppContent` composes: `useAppBootstrap` (`src/hooks/useAppBootstrap.ts`) owns language initialization, initial-route selection, the initial linking-enabled state, and splash hiding; `useAppStartup` (`src/hooks/useAppStartup.ts`) owns the one-time startup services (theme, notifications, notification actions, background sync, HealthKit observers); `useAutoSyncOnOpen` (`src/hooks/useAutoSyncOnOpen.ts`) for cold-start/foreground-return sync and the observer-yield window; `useAddSheetActions` (`src/hooks/useAddSheetActions.ts`) for AddSheet handlers and last-active-tab tracking. Error-boundary-wrapped `Safe*` screen components live in `src/navigation/safeScreens.tsx`.\n- Startup initializes theme, haptics, sounds, notification prefs, logs, timezone bootstrap, background sync, pending cache refreshes, fasting/hydration card visibility, and platform health observers.\n- Initial route comes from `getActiveServerConfig()` inside `useAppBootstrap`: no active config lands on `Onboarding`; otherwise users enter `Tabs`. A language-initialization failure is logged and never changes the route.\n- Deep links are enabled only after startup confirms `Tabs`, so widget links do not bypass first-run onboarding.\n- `CompareReport` is a stack route (`SafeCompareReport`, title `screens.compareReport`) opened from the Reports hub.\n- Navigation source of truth is `App.tsx` plus `src/types/navigation.ts`; update both and the linking config when routes change.\n- Root stack uses `@react-navigation/native-stack`. Tabs use `@react-navigation/bottom-tabs`.\n- Tabs are `Dashboard`, `Diary`, `Add`, `Library`, and `Settings`. `Add` is a center action in `CustomTabBar`, not a content screen.\n- Native iOS Liquid Glass tabs use `@bottom-tabs/react-navigation` in `src/components/TabsLayout.tsx`; each content tab is wrapped in its own `createNativeStackNavigator` so the tab path still gets native headers. The `Add` tab uses `role: 'search'`, which the iOS 26 SDK draws as the detached round button. The iOS 27 SDK no longer does, so `patches/react-native-bottom-tabs@1.4.0.patch` (a pnpm patch) leaves the role off on iOS 27+ and sets the tab controller's `prominentTabIdentifier` (the iOS 27 API for a separated trailing tab) to that tab instead. Drop the patch once the library does this itself.\n- `TabsLayout` switches at runtime via `useNativeIOSTabsActive()` (`services/nativeTabBarPreference.ts`): native tabs require iOS 26+ Liquid Glass support AND the user's `liquidGlassTabBarEnabled` preference (opt-in). Everything else renders `CustomTabBar` (`TAB_BAR_HEIGHT = 56`); `ActiveWorkoutBar` reads the native tab-bar height instead when native tabs are active.\n- When adding a root-stack screen, add the route to `RootStackParamList` and register a matching `<Stack.Screen>` in `App.tsx` with `createStackScreenOptions(...)` or equivalent explicit iOS native-stack header options.\n- Native header option/button builders live in `utils/nativeHeaderItems.ts` (`createIOSNativeHeaderOptions`, `createIOSSmallNativeHeaderOptions`, text/icon button items); header action colors come from `useHeaderActionColors`, which is Liquid-Glass-aware on iOS.\n- Root-stack screens with a screen-owned React header are automatically checked by `__tests__/navigation/nativeHeaderContract.test.ts`; do not add screen-specific native-header allowlists.\n- Root-stack screens with a screen-owned React header and a real back button must set `headerBackTitle` or `headerBackButtonDisplayMode: 'minimal'` in `App.tsx` so iOS back labels stay explicit or intentionally hidden; close/cancel modal headers do not need either option.\n- Declare screen headers with `useScreenHeader(config)` (`src/hooks/useScreenHeader.tsx`). One declarative descriptor (`title`/`nativeTitle`, `left`/`right` items of kind `back`/`dismiss`/`text`/`icon`/`primary`/`menu`, `busy`/`disabled`, `animateKey` for view↔edit cross-fades) renders both paths: on the native path it mirrors items into `unstable_header{Left,Right}Items` via a layout effect and returns `null`; on the custom path it returns the bar element for the screen to render. A `menu` item is a declarative dropdown (plain actions and/or titled single-select sections, optional accent-dot `showsBadge`): the native path builds a system UIMenu header button, the custom path renders the trigger plus an `AnchoredMenu` under it — one item list drives both, as in `FoodsLibraryScreen`'s ownership filter. Hook screens must not keep hand-rolled header code (no `unstable_header*Items` blocks or custom bars alongside the hook) — the contract test enforces this.\n- Path selection is `useNativeIOSHeadersActive()` (`services/nativeTabBarPreference.ts`): always false on Android; on iOS it is true below iOS 26 (classic native headers) and follows the Liquid Glass toggle on iOS 26+, so turning the toggle off swaps in the same screen-owned fallback headers Android renders.\n- One-accent rule: exactly one primary header action per screen (`kind: 'primary'` or `role: 'primary'`), enforced with a `__DEV__` throw; primary header save buttons fall back to the localized `t('common.save')` / `t('common.saving')` labels when no `label`/`busyLabel` is supplied. Footer-save forms mark their header Save `placement: 'native-only'` so the custom bar does not duplicate the sticky-footer button. `onPress` handlers dispatch through the hook's internal ref map — do not add per-screen handler-ref effects for native header buttons.\n- A **right-slot** `kind: 'primary'` press is wrapped in the shared synchronous duplicate-press guard (`utils/duplicatePress.ts`, same one `FooterSaveBar` uses): presses inside `DUPLICATE_PRESS_WINDOW_MS` collapse to one. `disabled`/`busy` are React state and have not committed when taps queued behind a blocked JS thread replay, so every queued press otherwise ran the handler again (#2191). The guard is deliberately time-based, not a latch on `busy`, because handlers that never report a pending state would leave the button dead. **Left-slot** primaries are exempt — that slot is navigation, and `CycleOnboardingScreen` uses the sugar for a wizard Back where repeated presses are intended. A screen that needs a rapidly repeatable right-slot action must not use `kind: 'primary'`.\n- If a root-stack screen is intentionally presented above `Tabs` instead of inside native-tabs mode, document it in `NATIVE_TABS_ROUTE_EXCLUSIONS` in `__tests__/navigation/nativeHeaderContract.test.ts` with a short reason.\n- Screens intentionally off the hook (e.g. `FoodSearchScreen`'s bespoke anchored-menu bar) must mirror custom actions with `unstable_header{Left,Right}Items` themselves, hide the screen-owned React header behind `useNativeIOSHeadersActive()` with a guard such as `{!usesNativeHeader && <Header />}`, and gate the `useLayoutEffect` that sets native header items on the same flag; otherwise iOS renders both headers.\n- When adding a tab, update `TabParamList`, `NativeTab.Screen`, and `FallbackTab.Screen`; for content tabs also add a tab-local native stack screen using `createIOSNativeHeaderOptions(...)`.\n- `__tests__/navigation/nativeHeaderContract.test.ts` enforces this native-header wiring. If it fails, fix the route/type/navigator alignment instead of weakening the test.\n- Current stack screens include onboarding/tabs, library/detail/form flows for foods/meals/meal plans/exercises/presets, food entry view/edit, meal type detail and copy, the family diary flows (`FamilyMembers`, `FamilyDiary`, `FamilyMealDetail`, and `FamilyCopyReview`), `EditBarcode`, food search/entry/scan/photo flow, workout/activity add/detail, exercise/preset search, settings subscreens, logs, sync, measurements, the progress photo flow (`ProgressPhotos`, `ProgressPhotoCompare`, and `ProgressPhotoTimelapse`), `ExerciseStatistics` and `CardioSession` (with `ActivityShare`, opened from its Share as image button: a 4:5 `ActivityShareCard` of stats and the GPS route over a picked photo or a transparent background, captured with Skia `makeImageFromView` in `services/activityShareImage.ts` and shared as a PNG; color from `utils/shareColor.ts`), `Shoes` and `ShoeForm`, fasting, mindfulness (`MindfulnessDetail` and `MindfulnessLog`), and `WhatsNew`.\n- `AddSheet` offers Food, Workout, Activity, Preset, Measurements, Scan Food, Progress Photos, Mood, Mindfulness, Ask Sparky, and Sync Health Data. Its Mood row opens `MoodLog`, and the Dashboard has a toggleable `mood` card (today's mood, 7-day trend) that opens it (mood faces, tags, note, and a mood-over-time chart; one entry per day via `POST /api/mood`). Its Progress Photos row opens `ProgressPhotos` on the diary's active date. Its Mindfulness row opens `MindfulnessLog` (`MindfulnessLogScreen`), a root-stack modal laid out like `MoodLog` and `MeasurementsAdd` (screen header, scrolling form, sticky `FooterSaveBar`, so the navigator owns Android's Back); `MindfulnessDetail` opens the same screen from the Log Mindful Session row on its Practice & Log tab and, with a `session` param, from Edit on its History tab, so it has no log form of its own. Session type names and icons come from `utils/mindfulnessSessionTypes.ts`, shared by the log screen, the detail screen and `MindfulnessCard`; the list is the shared `mindfulnessSessionTypeSchema`, and a stored type it does not have shows as Other and is kept on save. Paced Breathing and the Meditation Timer keep Finish & Save on screen while the save runs, so they go through `useSaveOncePerRun`: a run is saved once (presses are dropped until the practice is reset, taps replayed before that reset renders included), a failed save can be retried, and a save from a run closed while pending does not reset the next one. `MindfulnessLog` goes back after a save only while it is still focused, because it can be opened from a screen other than the tab host. Keep `AddSheet`'s present/dismiss refs intact to avoid Android re-present loops. Android's hardware Back in `AddSheet` does what the sheet's own Back link does: from the Exercise menu it returns to the main menu, from the main menu it closes the sheet.\n- Current stack screens include onboarding/tabs, library/detail/form flows for foods/meals/meal plans/exercises/presets, food entry view/edit, meal type detail and copy, the family diary flows (`FamilyMembers`, `FamilyDiary`, `FamilyMealDetail`, and `FamilyCopyReview`), `EditBarcode`, food search/entry/scan/photo flow, workout/activity add/detail, exercise/preset search, settings subscreens, logs, sync, measurements, the progress photo flow (`ProgressPhotos`, `ProgressPhotoCompare`, and `ProgressPhotoTimelapse`), `ExerciseStatistics` and `CardioSession`, `Shoes` and `ShoeForm`, fasting, mindfulness (`MindfulnessDetail` and `MindfulnessLog`), and `WhatsNew`.\n- `AddSheet` offers Food, Workout, Activity, Preset, Measurements, Scan Food, Progress Photos, Mood, Mindfulness, Ask Sparky, and Sync Health Data. Its Mood row opens `MoodLog`, and the Dashboard has a toggleable `mood` card (today's mood, 7-day trend) that opens it (mood faces, tags, a Journal box, and a mood-over-time chart). `useMoodCheckinsSupport` probes `GET /api/mood/date/<today>` with `X-Mood-Model-Version: 2` the first time the `mood` card, `MoodLog` or `MoodDay` mounts, and the answer is cached for the session: an array back means the server takes several check-ins a day and any other answer keeps the one-entry-per-day screen via `POST /api/mood`. A failed probe with nothing known shows a Retry on `MoodLog` and `MoodDay` (`components/mood/MoodSupportCheckFailed`) rather than either form, because the one-entry POST on a v2 server edits the day's latest check-in instead of adding one. On a v2 server the `mood` card shows one mood per day (the average of your own moods, else the Garmin or imported one), its body always opens `MoodDay` (the day's check-ins in time order with the day's averages; tap to edit in `MoodLog`, swipe or long-press to delete, Add check-in for a new one), which replaces itself with `MoodLog` for the day on an older server, and its \"+ Log\" (shown when no mood is logged) opens `MoodLog` for a new check-in. `MoodLog` takes an optional `checkinId` to edit; a new check-in for today goes on the profile zone's today even when the dashboard passed the device's day (`utils/moodDay.ts` `moodDayInZone`, which `MoodDay` also uses so its list shows that day; its redirect to the one-entry form keeps the requested day); each face writes the shared `moodFaceValue` (Sad 10 to Excited 90, as on the web); every field is optional but at least one is required, the mood can be left unset (tapping the selected face clears it), the time row opens `TimeSheet` and stays on the current time until set, and edit mode has Delete check-in. The mood report and history chart use one value per day. It also has optional 1-5 Energy and Focus taps (`components/mood/RatingSelector`) that are sent only when changed on the screen (absent means keep, `null` means clear); `useMoodRatingsSupport` applies only to the one-entry-per-day form; it hides them when the server's mood rows lack `energy_value` and shows them when there are no rows to tell, and a save whose response lacks the key shows a one-time \"update your server\" toast. Its Progress Photos row opens `ProgressPhotos` on the diary's active date. Its Mindfulness row opens `MindfulnessLog` (`MindfulnessLogScreen`), a root-stack modal laid out like `MoodLog` and `MeasurementsAdd` (screen header, scrolling form, sticky `FooterSaveBar`, so the navigator owns Android's Back); `MindfulnessDetail` opens the same screen from the Log Mindful Session row on its Practice & Log tab and, with a `session` param, from Edit on its History tab, so it has no log form of its own. Session type names and icons come from `utils/mindfulnessSessionTypes.ts`, shared by the log screen, the detail screen and `MindfulnessCard`; the list is the shared `mindfulnessSessionTypeSchema`, and a stored type it does not have shows as Other and is kept on save. Paced Breathing and the Meditation Timer keep Finish & Save on screen while the save runs, so they go through `useSaveOncePerRun`: a run is saved once (presses are dropped until the practice is reset, taps replayed before that reset renders included), a failed save can be retried, and a save from a run closed while pending does not reset the next one. `MindfulnessLog` goes back after a save only while it is still focused, because it can be opened from a screen other than the tab host. Keep `AddSheet`'s present/dismiss refs intact to avoid Android re-present loops. Android's hardware Back in `AddSheet` does what the sheet's own Back link does: from the Exercise menu it returns to the main menu, from the main menu it closes the sheet.\n- `useNavigationActionGuard` locks navigation-triggering actions while a native-stack transition is running (idle-callback unlock on re-focus, 5s safety release) so double-taps cannot queue duplicate screens; Library create actions use it.\n- `ActiveWorkoutBar` is mounted outside normal screen trees, uses the root navigation ref, and hides itself on modal/editor routes such as food search/forms/scan/photo, exercise search, workout/activity add, measurements, mindfulness log, and barcode edit.\n- Most screens are wrapped with `withErrorBoundary(...)`; `SettingsScreen` also uses section-level recovery so settings remain reachable.\n\n## Source Map\n\n- `src/components/` - reusable UI, charts, settings rows, custom tab bar, add sheet, workout HUD, form chrome, library rows, diary rows, serving sheets, food/workout editors, fasting UI, writeback UI, and `ui/` primitives.\n- `src/components/auth/` - MFA UI shared by onboarding, setup, and reauth.\n- `src/components/compare/` - the Compare report's sentence, span control, start state, answer/checks cards and dots/bars/over-time charts, plus the since-a-habit-started pieces (`CompareSinceRows`, `CompareSinceCard`, `CompareSinceChart`); copy comes from `utils/compareCopy.ts` / `utils/compareSinceCopy.ts`, the since line steps from `utils/compareSinceLine.ts`, metric labels from `constants/compareMetrics.ts`; the question model (spans, windows, suggested questions, recents helpers) and chart data (paired dots, trend line, over-time and since chart data) come from `@workspace/shared` (`shared/src/compare/question.ts`, `shared/src/compare/chartData.ts`).\n- `src/components/glp1/` - GLP-1 tracker cards (injection log, site body map, modeled level, pen/vial inventory, titration, daily check-in) used by `Glp1TrackerScreen`; hooks in `useGlp1.ts` (query keys sit under the `medications` root so `invalidateMedicationEntryCaches` refreshes them). Body-map geometry and the contract types come from `@workspace/shared`.\n- `src/components/medications/` - dose rows, the medication list row and the adherence section: `AdherenceOverviewCard` (stat tiles, ring, 14-day bars, streak), `MedicationChecklistCard`, `MedicationDoseListCard` (due and as-needed rows over `DoseRow`), `MedicationLogCalendar` (month grid, medication filter, legend, symptom dots; owns its own entries and symptom queries), `MedicationDayLog` (the selected day's entries with edit and delete) and `useMedicationStatusColors` (theme tokens for all of them). The screens are `MedicationAdherenceScreen` (route `MedicationAdherence`, opened from the \"Adherence & log\" row on `MedicationsListScreen`) and `MedicationEntryEditScreen` (route `MedicationEntryEdit`, a modal that takes the entry as a param). All adherence, streak, checklist and calendar-status maths comes from `@workspace/shared` (`medications/adherence.ts`, shared with the web Medications page); `hooks/useMedicationAdherence.ts` feeds it one entries window ending on the selected day, and `utils/medicationAdherence.ts` holds the mobile-only helpers (as-needed selection, month shifting, edited-time conversion). The `All | Meds | Supplements` filter helpers also live in shared (`medications/subtype.ts`). Every dose change goes through `invalidateMedicationEntryCaches`, which refreshes the diary card, adherence window, month calendar and daily summary together; injection rows are edited and deleted through the injection endpoints (`useUpdateInjection`, `useDeleteInjection`). The calendar's symptom dots read `useSymptomEntries`, need the separate symptoms permission (without it the dots are simply absent), and never show `source: 'cycle'` symptoms.\n- `src/screens/` - top-level route destinations: dashboard, diary, family member/diary/meal/copy-review flows, settings, sync, logs, Whats New, fasting, food search/scan/photo, library CRUD flows, workout/activity flows, and measurement entry.\n- `src/navigation/` - navigation-level modules such as `safeScreens.tsx`, the error-boundary-wrapped screen components registered in `App.tsx`. (`FoodPhotoFlow` lives in `src/components/`.)\n- `src/hooks/` - TanStack Query hooks, auth/connection hooks, library/search/mutation hooks, measurement/water/check-in hooks, fasting hooks, workout form hooks, widget sync, query client, query keys, and cache helpers.\n- `src/services/api/` - backend clients. `apiClient.ts` handles normal API auth/proxy headers; `healthDataApi.ts`, `aiSettingsApi.ts`, food-photo estimate, and other raw fetch paths must keep auth, proxy, timeout, and session-expiry behavior aligned.\n- `src/services/healthconnect/` - Android Health Connect reads, native aggregation, transformation, enrichment, preferences, and writeback.\n- `src/services/healthkit/` - iOS HealthKit reads, statistics aggregation, transformation, background delivery, preferences, and writeback.\n- `src/services/shared/` - platform-agnostic health helpers: the `collectHealthData` / `runForegroundSync` engine both orchestrators share, the per-run workout-telemetry budget and its reuse cache, Health Connect error classification, sample downsampling, day aggregation/transformation, preference factories, and permission migration/sets.\n- `src/services/` - platform health orchestration, writeback re-exports, background sync, auto-sync coordination, diagnostics, calculations, logging, storage, theme, haptics, sounds, notifications, food photo intro, meal selection, boolean preferences, card visibility, and workout drafts.\n- `src/stores/` - Zustand stores, including the persisted active workout/rest timer store.\n- `src/utils/` - date helpers, unit conversion, food details, meal nutrition, nutrient display, workout/session helpers, fasting formatting, numeric input, concurrency, sync utilities, duplicate-press guarding, photo estimate error mapping, and rate limiting.\n- `src/constants/` - meal, exercise, fasting, and nutrient metadata.\n- JS bridges to native modules live in `src/services/` (`CalorieWidgetBridge.ts`, `ExactAlarmBridge.ts`); there is no `src/native/` directory.\n- `plugins/`, `targets/widget/`, `targets/android-widget/`, `targets/android-exact-alarm/` - Expo plugins and widget/native extension sources.\n\n## React Query And Local State\n\n- Query setup lives in `src/hooks/queryClient.ts`; keys live in `src/hooks/queryKeys.ts`.\n- Default `staleTime` is `Infinity`, so mutations must explicitly invalidate or update affected caches.\n- `refreshHealthSyncCache` invalidates daily summary, measurements, sleep (`sleepDayQueryKey` / `sleepRangeQueryKey`), and exercise history after a health sync. Sleep is in that set because a partial observer upload would otherwise sit in the diary until the app is reloaded.\n- `useRefetchOnFocus(refetch, enabled)` is the standard focus-refresh hook.\n- `useCompare(question, window)` wraps `fetchCompare` on `compareQueryKey` for a day or week question; it is disabled until the question is askable (and for a since question), refetches on focus, and reports `serverTooOld` for a 404. `useCompareSince(question, habitStart, line)` wraps `fetchCompareSince` on `compareSinceQueryKey` (`habitStart` and `line` are null until picked and then left to the server); its `serverTooOld` is a 404 or a 400 whose JSON body names `details.mode` (an older server rejecting `mode=since`), while any other 400, such as a start out of range, is an ordinary error. `compareRecentQuestions` (appPreferencesStore, newest first, max 5) is written by `CompareReportScreen` once a distinct question, since questions included, has an answer.\n- `useFoodsLibrary` is an intentional exception with an infinite query, finite stale window, and `resetQueries(...)` refreshes so focus/pull refresh reloads page 1 instead of every cached page.\n- Meal mutations invalidate meals, recent meals, search, and details; food entry creation can affect recent meals.\n- Exercise/workout preset list/search/detail invalidation belongs in their mutation hooks.\n- `useUpsertCheckIn` updates measurement queries and calls `refreshHealthSyncCache(queryClient)`.\n- `useWaterIntakeMutation` fetches `waterContainersQueryKey`, persists the selected container, and optimistically updates `dailySummaryQueryKey(date)`.\n- Active-server switches clear React Query state before refetching connection state.\n- Error-boundary retry flows call `queryClient.resetQueries()`.\n- App-local toggles live in `stores/appPreferencesStore.ts` (Zustand `persist`, single AsyncStorage key `@SparkyFitness/app-preferences`): haptics, sounds, notifications, hydration/fasting card visibility, Ask Sparky and progress photos card visibility, the Liquid Glass tab bar opt-in, the active-workout metric column, the default rest period (`defaultRestSec`, edited in `WorkoutSettingsScreen`), mood check-in reminders (`checkinRemindersEnabled`, `checkinReminderTimes`, edited in `NotificationSettingsScreen` and offered once after the first check-in), hydration reminders (`waterReminderEnabled`, `waterReminderIntervalHours`, `waterReminderWindowStart` / `waterReminderWindowEnd`, edited in `NotificationSettingsScreen`), the Health Trends layout (`healthTrendOrder` + `hiddenHealthTrends`, edited in `HealthTrendsSettingsScreen`), the Reports layout (`reportOrder`, `hiddenReports`, `reportDefaultRange`, `hiddenReportSections`, `reportNutrientOrder`, `shownReportNutrients`, edited in `ReportsSettingsScreen`), the Compare report's recent questions (`compareRecentQuestions`, newest first, max 5, written by `CompareReportScreen` and read through `knownCompareQuestions`, which drops any a later build no longer knows), and the Apple Watch layout (`watchPageOrder` + `hiddenWatchPages`, the Goals page's `watchNutrientOrder` + `shownWatchNutrients`, and `watchSetInputStyle`, all edited in `WatchSettingsScreen`). The Health Trends pair is written through the single `setHealthTrendLayout(order, hiddenKeys)` setter rather than one per field, because one drag changes order and visibility together; a saved order is reconciled against `constants/healthTrends.ts` by `resolveHealthTrendOrder`, so registering a new graph needs no `STORE_VERSION` bump. Consume via selectors (`useAppPreferencesStore((s) => s.hapticsEnabled)`) plus generated setters; non-React code reads current values through helpers like `getDefaultRestSec()`. A legacy-aware storage adapter migrates the old per-key `@HealthConnect:*` values once. These preferences never sync to the server.\n\n## Health Sync\n\n- `src/services/healthConnectService.ts` is Android orchestration; `src/services/healthConnectService.ios.ts` is iOS orchestration. They are substantial platform implementations, not thin wrappers.\n- Both orchestrators batch metric reads with `runTasksInBatches`, a concurrency of 3, and per-metric timeouts. Preserve timeout and partial-error handling.\n- Bootstrap timezone state before sync. `ensureTimezoneBootstrapped(...)` runs at startup and `healthDataApi.ts` enforces it before upload.\n- Preserve `record_timezone` and `record_utc_offset_minutes` when available.\n- Manual sync, sync-on-open, foreground-return sync, background sync, and iOS observer-triggered sync share coordination logic. Preserve claim/in-flight guards and cooldown recording.\n- Health uploads are chunked. Simple measurements use large chunks; sleep sessions use smaller session chunks; exercise/workout records are grouped by source to match server delete-then-insert behavior.\n- Sync result objects include `syncErrors`; callers should surface partial failures and avoid advancing `lastSyncedTime` when any metric read failed.\n- `backgroundSyncService.ts` uses overlap windows for sessions and day-aligned rolling windows for cumulative metrics and nutrition. Do not collapse those into one naive window.\n- On iOS, cumulative metrics should use HealthKit statistics queries, not raw sample summation.\n- On Android, cumulative metrics (`Steps`, `Distance`, `ActiveCaloriesBurned`, `TotalCaloriesBurned`, `FloorsClimbed`) use Health Connect `aggregateGroupByPeriod` once per range. Native source-priority dedup should match Health Connect UI; do not reintroduce JS `Math.max` or source allowlist dedup.\n- Android read helpers return `{ records, error }` via `readHealthRecordsDetailed` and `aggregateCumulativeMetricByDayDetailed`; legacy wrappers unwrap only records.\n- Android exercise sessions are enriched with `aggregateRecord` for active, total, and basal calories plus distance and steps over the session window. Active/total calories start scoped to `dataOrigin`, while basal energy remains unfiltered; `total - basal` is compared as an active-energy candidate. Incomplete or implausible active/total pairs retry without the origin filter so Health Connect can apply source priority. Distance and steps always stay origin-scoped; never infer workout steps from an unfiltered clock-window query.\n- iOS exercise-session steps must come from statistics attached directly to the `HKWorkout`. Do not infer workout steps by querying all step samples in the workout's clock window.\n- iOS HealthKit locked-device failures surface as database-inaccessible warnings. Do not treat these as successful empty reads.\n- `app.config.ts` grants `android.permission.health.READ_HEALTH_DATA_HISTORY` so Android can read data older than 30 days.\n- Health Connect permission migrations belong in `services/shared/healthPermissionMigration.ts`, not UI-only state. iOS does not use that version: `refreshEnabledMetricPermissions` asks HealthKit through `hasUnrequestedHealthPermissions` and requests only when an enabled type was never asked about, so bump the version for Android permission changes only.\n- Core check-in measurements use `measurementsApi.ts` and `MeasurementsAddScreen`; preserve `upsertCheckIn` omitted-vs-null semantics.\n- \"Import Full History\" (`ImportHistoryScreen`, reached from `SyncScreen`) is a one-time resumable backfill: `backfillService.ts` walks 30-day day-aligned windows newest-first from start-of-today down to a probe-derived floor (`readEarliestRecord` on both providers), one upload per window, with a per-server checkpoint in `backfillCheckpoint.ts`. It never advances `lastSyncedTime` and never runs writeback; while it runs, `isBackfillRunning()` (autoSyncCoordinator) makes background sync skip.\n- The backfill's metric set is FROZEN in its checkpoint at first run; resume uses the frozen set verbatim and toggle changes require Start Over. Quota exhaustion, locked device, and app-inactive are expected mid-run stops — the checkpoint keeps them resumable.\n\n## Health Writeback\n\n- Writeback sends Sparky diary nutrition and hydration back to Apple Health on iOS and Health Connect on Android.\n- Platform split: `services/writeback.ios.ts` re-exports `healthkit/writeback.ts`; `services/writeback.ts` re-exports `healthconnect/writeback.ts`.\n- `runWriteback()` runs after inbound sync in its own try/catch. Writeback failures must not block inbound sync results.\n- Writeback is opt-in per metric and gated on write permissions. Android production permissions include `WRITE_NUTRITION` and `WRITE_HYDRATION`; other write permissions are dev-only.\n- Imported health entries are skipped to avoid echo loops. iOS sets the app bundle id as the own-source guard; Android relies on source metadata.\n- Per-day content-signature hashing skips unchanged days. Each run deletes prior tracked UUIDs then saves fresh records; failed deletes are retried next run.\n- `HealthDataWriteback` on `SyncScreen` owns the remove flow. `BottomSheetPicker` offers all-time purge or date range through `DateRangeSheet`; both call `removeWrittenData(range)` and clear tracking.\n- Inbound iOS nutrition sync reads food correlations with a rolling nutrition lookback and upserts by `(source, source_id)` server-side.\n\n## Native Patches\n\n- `react-native-health-connect` is declared as `^4.1.3`; the installed build is patched from the repo root via `patchedDependencies` in `pnpm-workspace.yaml`.\n- Patch file: `../patches/react-native-health-connect@4.1.3.patch`, one hunk. `PermissionUtils.mapPermissionResult` maps `ExerciseRoute` and `BackgroundAccessPermission` back to JS but not `ReadHealthDataHistory`, even though `parsePermissions` accepts it on the request path. Since `requestPermission` also resolves through `mapPermissionResult`, a granted history permission can never be observed, so `ensureHistoryReadPermission` always returns false and Import Full History believes it is capped at 30 days. Reported upstream as matinzd/react-native-health-connect#276; drop the patch if that lands.\n- The older 3.5.3 patch also forced `getTimeRangeFilterLocal` in `getAggregateGroupByPeriodRequest` (DST / local-day grouping) and made `ExerciseRouteResultType` string-valued. Both are gone: 4.1.3 uses the local filter at all 21 sites, and upstream is fixing the route enum in #274 by typing the read path as a string literal union instead — a better fix than ours, so `routeNeedsConsent` keeps accepting both forms and depends on neither declaration.\n- `@bacons/apple-targets@4.0.6` is patched via `../patches/@bacons__apple-targets@4.0.6.patch`, fixing two upstream bugs. First, its xcode pass matched \"its\" extension target by type with a fall-back to any same-type target, which adopted and corrupted the expo-widgets `ExpoWidgetsTarget` on a clean prebuild; the patch scopes the match to an exact product-name hit. Second, the existing-target update path crashed every non-clean prebuild (EvanBacon/expo-apple-targets#201): removing the old build configuration list's referrers cleared `target.props.buildConfigurationList`, which the next line then dereferenced; the patch holds the list in a local and iterates a copy of its configurations so none are skipped mid-removal.\n- `@bacons/apple-targets` is held at 4.0.6 on purpose: it has a 5.x, and moving to it invalidates the patch file. `expo install --fix` does not touch either patched package (neither is an Expo SDK package), so SDK upgrades leave both patches applying cleanly.\n- `expo-health-connect` was removed. It only ever supplied an Expo config plugin, and upstream archived it: everything it did ships inside `react-native-health-connect` as of v4, which now writes both the `ACTION_SHOW_PERMISSIONS_RATIONALE` intent filter (Android 13 and below) and the `ViewPermissionUsageActivity` alias (Android 14+). Installing both causes Android build failures from duplicate classes.\n- After changing a patch or upgrading a patched package, run `pnpm install` from the repo root and then `npx expo prebuild --clean` from mobile before native validation.\n\n## Food, Meals, Units, And Photo Estimates\n\n- Food search spans local foods, online providers, meals, barcode scan, label scan, and AI photo estimates. Keep `FoodSearchScreen`, `FoodScanScreen`, `FoodEntryAddScreen`, `FoodFormScreen`, `FoodPhotoFlow`, and route params aligned.\n- `LibraryScreen` is the hub for saved Foods, Meals, recurring Meal Plans, Exercises, and Workout Presets. Full lists live in `FoodsLibraryScreen`, `MealsLibraryScreen`, `MealPlansScreen`, `ExercisesLibraryScreen`, and `WorkoutPresetsLibraryScreen`.\n- Food detail/edit flow: `FoodDetailScreen`, `FoodFormScreen`, `FoodForm`, `FoodUnitSelectorSheet`, `useFoodVariants`, `useFoodsLibrary`, `useDeleteFood`, `foodsApi`, and `utils/foodDetails.ts`.\n- `FoodForm` supports equivalent serving sizes grouped by nutrient signature, auto-scale nutrition, compatible unit conversion via `convertServingSizeOnUnitChange`, optional AI cross-category unit conversion, custom nutrients, and caller-provided `headerChildren`.\n- `EditBarcodeScreen` lets users add or remove extra barcodes for a saved food. Keep `FoodDetailScreen`, `EditBarcodeScreen`, `foodsApi`, and the `EditBarcode` route params aligned.\n- Meal templates use `MealAddScreen`, `MealDetailScreen`, `FoodSearch` / `FoodEntryAdd` with `pickerMode: 'meal-builder'`, and `services/mealBuilderSelection.ts` for pending ingredient handoff.\n- Recurring meal plans use `MealPlansScreen`, `MealPlanFormScreen`, `useMealPlans`, `mealPlansApi`, and `utils/mealPlanForm.ts`. Plans can schedule any meal already visible in the user's meal library, including family/public meals, but the plan itself remains private to its owner.\n- Workout plans (the schedule the watch's \"Scheduled today\" and the diary banner read) are created and edited from `Library > Workout plans` with `WorkoutPlansScreen`, `WorkoutPlanFormScreen`, `useWorkoutPlans`, `workoutPlansApi.ts` and `utils/workoutPlanForm.ts`, modeled on the meal plan screens. The form supports weekly and sequential plans and assigns saved workouts (presets); a single-exercise assignment made on the web is preserved on save but only editable there. Switching the schedule type clears the assignments, as the server expects new ones with a type change.\n- Logged-meal grouped diary entries use `foodEntryMealsApi`, `FoodEntryViewScreen`, and `EditLoggedMealScreen`. Preserve stored component nutrition snapshots when editing.\n- `MealTypeDetailScreen` owns single-meal-type day views and copy-to-another-day via `useCopyFoodEntries`; be careful with custom meal types and synthetic buckets.\n- External food providers use provider-agnostic v2 endpoints where possible. Provider categories and barcode support come from server config; do not hardcode provider type allowlists unless preserving an explicit fallback.\n- \"All Providers\" aggregated search (`useAllProvidersSearch`) fans one debounced term out across every active provider in parallel — one `useQueries` entry per provider, results projected in the `combine` callback for structural sharing. Providers stream in independently; a slow or failing provider must not block the others. Open Food Facts calls go through the shared rate limiter.\n- Label scan tries Apple's on-device model first: `modules/on-device-nutrition` (Swift, `FoundationModels` with the label photo as an `Attachment` plus its text from a Vision `VNRecognizeTextRequest` run in the module (greedy sampling), iOS 27+ with Apple Intelligence; compiled out under `compiler(<6.4)`) is wrapped by `services/onDeviceLabelScan.ts`, which validates the extraction (calories, macros, a per-serving size, and any optional nutrient the model filled in must appear as numbers in the returned `ocr_text`, ranges, sugars/fiber <= carbs, energy vs macros) and returns null on any failure or when the device-local `onDeviceLabelScanEnabled` preference (Settings → AI, `AiSettingsScreen`, shown only when the model is available; it also shows the active server provider, read-only) is off, so `FoodScanScreen.handleUsePhoto` falls back to the server `scanNutritionLabel`. The food form filled in from a scan carries `labelScanSource` and shows `LabelScanSourceBanner`: it says which AI read the label and offers the other one (`useRetryLabelScan` reads the kept photo from `services/labelScanSession.ts` again and replaces the form; coming back to the phone needs the setting on and the model available). The Swift side has not been compiled against the iOS 27 SDK from CI-less environments; verify on an iOS 27 device after `npx expo prebuild --clean`.\n- Photo mode is hidden in meal-builder mode because photo estimates log to the diary.\n- `FoodPhotoFlow` is a modal native stack and wraps itself in a local `KeyboardProvider`.\n- Photo availability fetches `GET /api/chat/ai-service-settings/active` through `aiSettingsApi.ts`; food photo is attempt-all, so `isFoodPhotoAvailable` gates only on a configured provider, not a specific provider type.\n- Estimation posts to `POST /api/foods/estimate-food-photo` through `estimateFoodPhoto(...)` in `externalFoodSearchApi.ts` and uses typed `FoodPhotoEstimateError` codes from `@workspace/shared`.\n- Food-photo request/response changes cross package boundaries: update shared schema and server route/service with mobile.\n- Keep `auto_scale_online_imports` separate from Open Food Facts-specific scaling preferences in `FoodSettingsScreen`.\n- Nutrient management lives under Food Settings: `CustomNutrientsScreen` / `CustomNutrientFormScreen` (create, edit, delete; renames and deletes invalidate display, goals and daily-summary caches since the nutrient name is the key), and `NutrientDisplaySettingsScreen` (`mobile` platform only, and only the view groups a mobile screen reads: `food_database`, which `FoodNutrientBreakdown` applies to the food, meal and entry nutrient lists, and `report_tabular`, which `DailyNutritionDetailsScreen` applies; `summary` and `diary` keep their Dashboard/Diary screens, and `quick_info`, `goal` and `report_chart` are not offered until a mobile screen reads them). A nutrient's goal direction is edited on the Goals screen.\n\n## Exercise, Workouts, And Fasting\n\n- Exercise and workout preset flows use `ExerciseSearch`, `PresetSearch`, detail/form screens, paginated/search hooks, mutation hooks, and shared workout payload helpers in `utils/workoutSession.ts`.\n- Session responses are discriminated unions from `@workspace/shared`: preset workouts and individual activity sessions have different shapes. Keep detail/edit screens type-safe.\n- Workout/activity drafts are persisted by `workoutDraftService`; `useWorkoutForm`, `useActivityForm`, and `useDraftPersistence` own form state.\n- Exercise selection returns via `CommonActions.setParams` and a nonce pattern through `useSelectedExercise`.\n- Adaptive coaching (#1560): every Replace action passes `replaceFor` (built by `utils/exerciseReplace.ts`) so `ExerciseSearch` opens on a ranked **Suggested** tab (`components/ExerciseAlternativesList.tsx`, `GET /api/v2/exercises/:id/alternatives`); catalog picks import through `importExercise` first. End-of-workout feedback is `components/WorkoutFeedbackCard.tsx` (on `WorkoutCompleteScreen` and `WorkoutDetailScreen`) backed by `useWorkoutSessionFeedback`, which autosaves serially so the last answer wins. `useLiveCoachingSignals` (mounted in `ActiveWorkoutScreen`) captures signals into `activeWorkoutStore.coachingSignals` (`null` = asked, no history); `resolveLiveAssumedSetValues` applies the shared `decideAdaptiveAdjustment` rules unless the entry is in `declinedAdaptive`, and `AdaptiveSuggestionBanner` always explains the change with a \"Use my usual\" undo. The `adaptive_workout_suggestions` switch in `WorkoutSettingsScreen` is an account setting; `duckMusicDuringCues` (Lower music during cues) is device-local and drives `beginCueDucking`/`endCueDucking` in `services/sounds.ts` — with it off, the audio session is never touched.\n- Bodyweight exercises (#56) use the `bodyweight_reps` modality: a set's weight is signed (+ added, − assistance, blank = body weight alone). Weight inputs parse it with `parseSignedDecimalInput` (`utils/numericInput.ts`; the base `parseDecimalInput` still rejects a sign) only when the modality is `bodyweight_reps`, including the save builders, and switch to a keyboard with a minus key. Volume and the e1RM/10RM columns go through `setVolumeKg` / `setLoadKg` in `utils/workoutSession.ts`, which delegate to the shared `effectiveLoadKg` rule, with body weight from `hooks/useBodyWeightKg.ts` (the latest check-in on or before the day, fetched only when an exercise on screen is bodyweight). Live PRs stay on the added weight, an unweighted set ranking as +0.\n- Weighted carries and loaded holds (#127) use the `weight_distance` / `weight_duration` modalities. Carry distance is typed in metres (yards when the app distance unit is miles) and converted with `carryDistanceFromKm` / `carryDistanceToKm` from `@workspace/shared` (`utils/carryDistance.ts`); always go through `setDistanceFromKm` / `setDistanceToKm` / `modalityStoresDuration` / `modalityStoresDistance` in `utils/workoutSession.ts` rather than branching on modality. Carry distance is excluded from the workout's cardio distance total. The activity edit flow (`EditableSetRow` / `buildActivitySetsPayload`, which takes the distance unit) edits weight plus seconds or distance for these types.\n- Watch support for them: a loaded hold is armed as `timed` + `weighted` (weight box beside the countdown/stopwatch), a carry as `carry` with `targetDistanceKm`; the watch shows and edits the distance in the phone's unit (metres, or yards when the distance unit is miles; the context carries `distanceUnit`, read as `effectiveCarryUnit`) while `SetValues.distanceKm` / `CompletedSet.distanceKm` stay km on the wire, and `useWatchWorkoutBridge` writes it back as `distance`. The distance keypad accepts a tenth. A pending distance edit is dropped if `distanceUnit` changes before that edit commits, and an open distance keypad is dismissed, same as a weight-unit change drops a pending weight edit. `resolveWatchSetTargets` is the single place those targets are built.\n- Rest timer state lives in `stores/activeWorkoutStore.ts`; notifications are scheduled through `services/notifications.ts`. The rest-complete ping carries a background \"Complete Set\" action (`rest-complete` category): responses are routed to `completeActiveSetIfReady` by `initWorkoutNotificationActions` (exported from the store, wired in App startup — the response listener cannot live in `notifications.ts` without a store↔service import cycle), and stale delivered pings are swept when the next rest is scheduled. Exactly one cue fires per rest: the in-app chime (`services/sounds.ts`) while the app is active, otherwise the ping's own sound. Anything that stands down in favour of the chime must test `willPlayRestCompleteSound()` (preference **and** `AppState`), never the preference alone — the notification handler also runs for frontmost-but-inactive states (screen locking, app switcher), and `markRestReady` cancels the due ping only while the app is active so the deadline flip cannot race the OS delivery. The ping ships at the `timeSensitive` interruption level so a Focus mode cannot silence it.\n- Set IDs are preserved server-side across workout edits so the active w","isInternal":false,"tokens":11363,"sizeBytes":50028},{"name":"AGENTS.md","path":"SparkyFitnessServer/AGENTS.md","rawUrl":"https://raw.githubusercontent.com/CodeWithCJ/SparkyFitness/HEAD/SparkyFitnessServer/AGENTS.md","title":"SparkyFitnessServer - AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"# AGENTS.md\n\n_Last updated: 2026-10-22_\n\nSparkyFitness Server is the backend API package for the SparkyFitness monorepo. Use this file as the primary guide for work inside `SparkyFitnessServer/`.\n\n**Quick Links for AI Tools:** See `../agent-docs/README.md` for:\n\n- `file-and-domain-reference.md` — Where to find server code by feature\n- `testing-patterns.md` — How to test routes, services, repositories, and RLS\n- `architecture-permissions.md` — Permission types and RLS patterns\n- `new-migration-checklist.md` — 8-step database change checklist\n\nIf a task also touches `shared/`, the frontend, or the mobile app, read the relevant package guide before editing outside this directory. Use `../AGENTS.md` for monorepo-level context.\n\n## Scope\n\n- This file is for package-local work in `SparkyFitnessServer/`.\n- Keep changes inside this package unless the task clearly crosses package boundaries.\n- This is the single source of truth for the package; `CLAUDE.md` just imports it via `See @AGENTS.md`.\n- Do not invent alternate boot paths, duplicate route registries, or parallel migration flows when the current startup path already covers the behavior.\n\n## Current Snapshot\n\n- Dev boot path: `pnpm start` -> `nodemon` -> `tsx index.ts`\n- `index.ts` loads `../.env`, applies file-backed secrets, runs preflight checks, calls `initializeDatabase()` for migrations and RLS policies, then imports `SparkyFitnessServer.ts`\n- Main app shell: `SparkyFitnessServer.ts`\n- Stack: Express 5, PostgreSQL via `pg`, Better Auth, Zod, TypeScript 6, Vitest 5, ESLint 10\n- Module system: ESM with `type: \"module\"` and `moduleResolution: \"NodeNext\"`\n- The package is now effectively TypeScript-first; almost all source files are `.ts`\n- Main domains: food and meal tracking, exercise logging, health and sleep data, sleep science, fasting, medications, symptom and episode tracking, mood, menstrual cycle and pregnancy, reporting, AI chat, onboarding, identity, admin tooling, and external provider integrations\n\n## Verified Commands\n\n```bash\npnpm start\npnpm run validate\npnpm run typecheck\npnpm run lint\npnpm run lint:fix\npnpm run format:check\npnpm run format\npnpm test\npnpm run test:watch\npnpm run test:coverage\npnpm run test:ci\npnpm exec vitest run tests/mealRoutes.test.ts\npnpm exec eslint routes/v2/foodRoutes.ts services/foodCoreService.ts\n```\n\n- `pnpm start` uses hot reload through `nodemon`; `nodemon.json` ultimately executes `tsx index.ts`\n- `pnpm run validate` runs typecheck, lint, and Prettier check together\n- `pnpm test` runs `vitest run`\n- The backend default port is `3010` unless `SPARKY_FITNESS_SERVER_PORT` overrides it\n- For targeted test runs, prefer `pnpm exec vitest run tests/<name>.test.ts`\n\n## Source Map\n\n- Food assistant state: owner-only preferences, recoverable tasks and operation history use `FoodAssistant.api.zod.ts` / `FoodAssistant.zod.ts` in shared. Server: `routes/v2/foodAssistantRoutes.ts`, `services/foodAssistantService.ts`, `models/foodAssistantRepository.ts`, `ai/tools/foodAssistantTools.ts`. Web: `src/components/ai/FoodAssistantState.tsx`, `src/api/Chatbot/foodAssistantService.ts`.\n\n- `index.ts` - real dev entrypoint; loads env, secrets, and preflight checks before booting the app\n- `SparkyFitnessServer.ts` - Express app shell, route mounting, Swagger/ReDoc, startup, graceful shutdown\n- `services/backgroundJobScheduler.ts` - starts every scheduled background job at startup, including the demo reset; provider syncs are listed in `services/providerSyncScheduler.ts`; `SPARKY_FITNESS_DISABLE_SCHEDULED_JOBS=true` skips them all\n- `auth.ts` - Better Auth configuration, plugins, session behavior, SSO provider syncing\n- `routes/` - primary HTTP route surface\n- `routes/v2/` - newer typed route surface; pair these changes with `schemas/`\n- `routes/v2/openFoodFactsContributionRoutes.ts` - owner-only single-food preview and explicit photo-backed publication; background contributions are disabled for this release\n- `routes/v2/symptomRoutes.ts` - generic symptom tracking (`symptoms` permission): definitions (`/custom`), the pick-list library (`/options`), entries and episodes (`/entries`, `/entries/ongoing`, `/entries/:id/end`, `/entries/:id/severity`), photos, and symptom-free days. Logic lives in `services/symptomService.ts` over `models/symptomRepository.ts` and `models/symptomOptionRepository.ts`; the request/response contract is `../shared/src/schemas/api/Symptoms.api.zod.ts`\n- `routes/reportRoutes.ts` `GET /reports/training-consistency` - the consistency view (#59): `reportService.getTrainingConsistency` reads a fixed 26 weeks of exercise entries and hands them to the shared `buildTrainingConsistency`, so web and mobile show the same streak and weekly sets (`reports` permission)\n- `routes/reportRoutes.ts` `GET /reports/training-load` - acute (7 days) against chronic (4-week average) workout load (#250): `reportService.getTrainingLoad` reads every logged workout through `reportRepository.getTrainingLoadSessions` and the person's heart rate zones (`heartRateZoneService.resolveZonesForUser`), and hands them to the shared `computeTrainingLoad` (`reports` permission)\n- `routes/v2/reportRoutes.ts` - weekly alcohol rollup and the zero-padded hydration/caffeine/alcohol range used by the Trends charts (`reports` permission)\n- `routes/v2/reportRoutes.ts` `GET /compare` - does A go with B (`/api/v2/reports/compare`, owner-only via `requireSelfActor`): parses `compareQuerySchema`, `services/compareService.ts` (`getCompare`) loads both metrics through `models/compareRepository.ts` (today and the current week never counted) and hands them to the shared analysis in `../shared/src/compare/` (`stats.ts`, `metrics.ts` catalog, `analyze.ts` day/week pairing); week mode uses `utils/firstDayOfWeek.ts` and training load is weighted by the person's zones (`heartRateZoneService.resolveZonesForUser`); `mode=since` (before vs since a habit started) parses `compareSinceQuerySchema` instead and calls `getCompareSince`, which hands the shared analysis in `../shared/src/compare/since.ts` the habit over the last 730 days plus two weeks (the analysis holds a suggested start to the last 730 days, `SINCE_START_MAX_DAYS_AGO`, and offers none for a habit already kept at that limit); contract `../shared/src/schemas/api/Compare.api.zod.ts`. The AI tool `ai/tools/compareTools.ts` (`sparky_compare_metrics`, `reports` category, read-only for MCP keys) asks the same two service functions through the shared query schemas and renders the result as text; its English metric names and wording live in that file\n- `routes/v2/nutritionKineticsRoutes.ts` - active-caffeine estimate and bedtime cutoff (`diary` permission)\n- `routes/v2/workoutCoachingRoutes.ts` - adaptive coaching (#1560): session feedback (`workout_feedback`), the per-user `adaptive_workout_suggestions` setting (owner-only write), and recent-history signals (`diary` permission). `GET /v2/exercises/:id/alternatives` (ranked substitutes) lives in `routes/v2/exerciseRoutes.ts`\n- `routes/v2/routeRoutes.ts` - the person's saved routes (`/api/v2/routes`: `GET` summaries, `POST` a drawn route from `points` (+ optional `cues`), `POST /import-gpx` with the file text, `POST /from-activity/:exerciseEntryId` from a recorded GPS track, `GET/PATCH/DELETE /:id` (PATCH renames, changes the activity or replaces the line: `points` + optional `cues` go through `prepareRoute`, which recomputes distance and climb in the same UPDATE and leaves `source`, `source_exercise_entry_id` and `created_at` alone; cues need `points` and every cue index must be below the point count), `GET /:id/gpx` download; 200 per person); owner-only (`requireSelfActor`, `routes` owner policy). `models/routeRepository.ts` stores them; `utils/routePrep.ts` (`prepareRoute`) drops repeated points, measures distance and climb on the full track, thins to 2000 points and re-points cues. Geometry, GPX read/write and the contract are shared: `../shared/src/utils/routeGeometry.ts`, `routeGpx.ts`, `../shared/src/schemas/api/Route.api.zod.ts`\n- `routes/v2/routeRoutes.ts` generation endpoints - `GET /api/v2/routes/generate/capabilities` and `POST /api/v2/routes/generate` (preview only, never saves; owner-only; 10 requests per user per 10 min via `utils/userRateLimiter.ts`). Orchestration is `services/routeGenerationService.ts` (loop resizing, shared `prepareRoute` thinning); the engine sits behind `RoutingProvider` in `services/routing/`. `routingConfig.ts` `resolveRoutingProvider(userId)` picks the engine per request: the user's active `valhalla` external provider first (own, then admin-public, then family-shared via `externalProviderRepository.getActiveProviderWithKeyByType`), else none. The base URL is re-checked with the food-provider network policy and DNS-guarded for non-admin owners, the key comes from the encrypted `app_key` and is sent as `api_key`, and nothing is cached so a new provider applies at once. There are no environment variables for routing. Every engine request carries a `SparkyFitness/<version>` `User-Agent`. The type is seeded by migration `20261021120200_add_valhalla_routing_provider_type.sql` and registered in `BASE_URL_FETCHING_PROVIDER_TYPES` in `externalProviderService.ts`; `valhallaProvider.ts` talks to the engine, pure `valhallaManeuvers.ts` decodes polylines and maps maneuvers to cues, pure `loopPlanner.ts` plans loops. `GET /generate/capabilities` reports `{ available, maxAlternatives: 1, style (all false), stops: false, source: 'provider' | null }` from `getRouteGenerateCapabilities`; the server returns one route per request and `POST /generate` answers `{ route }`. Saving a generated route posts it to `POST /api/v2/routes` with `source: 'generated'` (the only source a client may choose besides `drawn`; migration `20261021120100_allow_generated_route_source.sql` widens the check). Add an engine by implementing `RoutingProvider` and a case in `routingConfig.ts`; never log the provider error object (it can carry the key).\n- `routes/v2/runProgramRoutes.ts` - the person's run program and place in it (`/api/v2/run-program`: `GET`, `PUT` to start, switch on or off, move, or set the Diary schedule (`run_days`, `start_date`, `end_date`), `POST /complete` which also records the person's local `day` as `last_done_date` and, given `entry_id`, appends `{index, entry_id, at}` to `completion_log` (newest 200 kept; cleared on a program switch) so the app can show a diary run as part of the program, `POST /effort` which saves a 1 to 10 rating for a workout already done (`rateRunProgramWorkout`: under the row lock, replaces an earlier rating of the same index, keeps the last 40 in `effort_log`, refused for a workout not yet done or another program; a program switch clears it); the tool's `get_run_program` lists recent ratings and, through shared `suggestFromEffort`, notes two hard or two easy in a row); `POST /realign` rebuilds the rest of the plan to end on the goal day through shared `realignProgram`/`applyRealign` (`../shared/src/utils/runProgramRealign.ts`): without `confirmed: true` it only returns the preview (and the options when the goal is unrealistic) and saves nothing; confirmed, it recomputes under the `adjustRunProgram` row lock (the callback also receives the whole stored `program`), refuses a stale preview `token` with 409 and an unrealistic goal without `force` with 400; the person's local `today` comes from the body or their timezone. After a realignment the stored workouts no longer line up with the built-in definition, so adjusts measure against `builtInBaseline` (built-in workouts only while the lengths match). Owner-only (`requireSelfActor`, `run_programs` owner policy). The person's own plans live in `custom_run_programs` (`GET/POST /custom`, `PUT/DELETE /custom/:id`, 30 per person, up to 364 workouts of timed steps, program id `custom:<uuid>`): starting one through `PUT` copies it into `run_programs` (with `name`/`activity`), editing one also updates the active copy and keeps the place, deleting keeps the active copy. `models/runProgramRepository.ts` seeds the stored `workouts` from the built-in program in `../shared/src/utils/runPrograms.ts`, completes the due workout in one statement so a repeat cannot skip one, and `adjustRunProgram` applies a change under a row lock with a log entry. The change rules (only upcoming workouts, running eased up to 30% or raised at most 10%) are `../shared/src/utils/runProgramAdjust.ts`, shared by the AI tool `ai/tools/runProgramTools.ts` (`sparky_manage_run_program`; every change needs `confirmed=true`; `set_run_program_schedule` changes the Diary days (names or 0 to 6), first day and goal day through the same upsert; `realign_run_program` (fields `force`, `distance`, `confirmed`) previews the same plan and applies it only with `confirmed=true`, and says nothing was changed for an unrealistic goal; `get_run_program` adds a line when the plan has drifted from the goal day; only `get_run_program` is read-only for MCP keys, and the realign POST is not on the read-only allowlist in `utils/apiKeyScope.ts`) and the contract is `../shared/src/schemas/api/RunProgram.api.zod.ts`\n- `routes/v2/heartRateZoneRoutes.ts` - the person's heart rate zone settings (`GET`/`PUT /api/v2/heart-rate-zones`; max and resting heart rate, `percent_max` / `heart_rate_reserve` / `custom` method, custom floors; `reset: true` restores defaults). Owner-only (`requireSelfActor`, also on reads). Logic in `services/heartRateZoneService.ts` over `models/heartRateZoneRepository.ts` (columns on `user_preferences`); `resolveZonesForUser(userId, observedMaxBpm?)` is what stored time in zone (`healthDataHandlers.persistWorkoutTelemetry`, `exerciseEntryService.attachWatchTelemetryToExerciseEntry`) and training load use. Zone maths is `../shared/src/utils/heartRateZones.ts`; the contract is `../shared/src/schemas/api/HeartRateZones.api.zod.ts`. Stored `exercise_entry_hr_zones` rows carry `zone_method` (`device` for Garmin) and are never recomputed when settings change.\n- `routes/v2/mindfulnessRoutes.ts` - mindfulness & meditation sessions tracking (`checkin` permission): day summaries (`/day-summary`), session logs and telemetry (`/entries`, `/entries/:id`). Logic lives in `models/mindfulnessRepository.ts`; the request/response contract is `../shared/src/schemas/api/Mindfulness.api.zod.ts`\n- `routes/v2/shoeRoutes.ts` - running-shoe inventory (`diary` permission): CRUD on `/api/v2/shoes` and `PUT /shoes/assignments/:exerciseEntryId` to link or unlink a diary entry (`exercise_entries.shoe_id`). Logic lives in `models/shoeRepository.ts`; mileage is `starting_distance_km` plus the sum of linked entries' `distance` (km); the contract is `../shared/src/schemas/api/Shoes.api.zod.ts`\n- `routes/auth/` - auth-specific route fragments mounted through `routes/authRoutes.ts`\n- `services/` - business logic and orchestration\n- `models/` - PostgreSQL repositories and persistence helpers\n- `middleware/` - auth, permissions, uploads, and shared Express middleware\n- `utils/uploadsPath.ts` - the uploads root plus the resolver and containment guard for stored `file_path` values; use it instead of re-deriving `SPARKY_FITNESS_CUSTOM_UPLOADS_DIRECTORY`\n- `utils/oauthState.ts` - server-issued single-use OAuth `state` nonces for provider linking (`issueOAuthState`, `persistOAuthState`, `claimOAuthState`); use it instead of hand-rolling a state value\n- `utils/outboundHttp.ts` - process-wide outbound HTTP defaults, applied from `index.ts`: the axios request timeout and the per-address-family connect attempt timeout (`net.setDefaultAutoSelectFamilyAttemptTimeout`). Both are fixed constants on purpose - do not add env overrides, and read the sizing note there before changing either, because the two values interact\n- `utils/errors.ts` - `ValidationError` plus `describeError(error)`; prefer it over `error.message` when logging any caught value, because an `AggregateError` or a non-Error throw renders as an empty string\n- `middleware/requireSelfMiddleware.ts` - `requireSelfActor`, which rejects a switched/delegated context outright; attach per-route to account-linking routes\n- `integrations/` - provider adapters and ingest pipelines\n- `schemas/` - Zod route schemas\n- `types/` - TypeScript declarations, including `Express.Request` augmentation\n- `db/` - pool management, grants, migrations, and RLS policies\n- `config/` - logging and Swagger config\n- `utils/` - startup helpers, CORS, permissions, timezone loading, OIDC helpers, migration helpers\n- `ai/` - AI provider configuration (`config.ts`), the unified provider-dispatch helper (`providerDispatch.ts`), and the in-process chatbot tool registry (`ai/tools/`)\n- `security/` - encryption utilities (`encryption.ts`)\n- `validation/` - legacy express-validator rules for a few older routes (new routes use Zod schemas)\n- `constants/` - shared constants and supporting package data\n- `tests/` - Vitest suites plus a few utility scripts\n- `devdocs/` - local notes and debugging artifacts when present\n\nWhen searching, ignore noisy/generated directories unless you explicitly need them:\n\n- `node_modules/`\n- `coverage/`\n- `uploads/`\n- `temp_uploads/`\n- `backup/`\n- `mock_data/`\n\n## Architecture\n\n### Boot and App Shell\n\n- `index.ts` is the true local boot path used by `pnpm start`; do not bypass it for normal development because it performs env loading and preflight work\n- `SparkyFitnessServer.ts` creates the Express app, configures static upload serving, mounts auth interception, registers routes, exposes API docs, starts background jobs through `services/backgroundJobScheduler.ts`, and handles graceful shutdown\n- Startup order matters:\n  - `index.ts`: await `initializeDatabase()`, which applies pending migrations and then reapplies `db/rls_policies.sql` under a PostgreSQL advisory lock, **before** importing `SparkyFitnessServer.ts` (and therefore `auth.ts`)\n  - upsert env-configured OIDC provider\n  - mount Better Auth\n  - sync trusted SSO providers\n  - register cron jobs\n  - optionally promote `SPARKY_FITNESS_ADMIN_EMAIL` to admin\n  - start listening\n- Public API docs live at:\n  - `/api/api-docs/swagger`\n  - `/api/api-docs/redoc`\n  - `/api/api-docs/json`\n- If you change public endpoints, keep Swagger JSDoc and `config/swagger.ts` coverage accurate\n\n### Environment and Secrets\n\n- Runtime `.env` is expected at `../.env`\n- The tracked template lives at `../docker/.env.example`\n- `utils/secretLoader.ts` loads `*_FILE` secrets before preflight validation\n- Three layers decide whether a variable has to be set, and they are easy to confuse:\n  1. **`utils/preflightChecks.ts` refuses to start** without these four, because none has a safe default:\n     - `SPARKY_FITNESS_DB_PASSWORD`\n     - `SPARKY_FITNESS_FRONTEND_URL`\n     - `SPARKY_FITNESS_API_ENCRYPTION_KEY`\n     - `BETTER_AUTH_SECRET`\n  2. **`preflightChecks.ts` fills in a default** for `SPARKY_FITNESS_DB_HOST` (`sparkyfitness-db`), `SPARKY_FITNESS_DB_NAME` (`sparkyfitness_db`), `SPARKY_FITNESS_DB_USER` (`sparky`) and the two app-role variables, logging which one it defaulted. These matter only outside Compose, which supplies them itself.\n  3. **`docker/docker-compose.prod.yml` supplies a value** for almost everything via `${VAR:-default}`, so a Compose deployment only ever has to set the four in (1). Keep the defaults in (2) identical to Compose's: a value that differs between them silently points the server at a database other than the one Compose created.\n- `SPARKY_FITNESS_APP_DB_USER` and `SPARKY_FITNESS_APP_DB_PASSWORD` are soft-required: preflight defaults the user to `sparky_app` and mints a password when absent, and `utils/dbMigrations.ts` creates the role or re-syncs its password so the two always match. It probes a connection as that role first, so an externally pre-created role is left alone and the owner does not need `CREATEROLE` — but only while `SPARKY_FITNESS_APP_DB_PASSWORD` still authenticates. If it is absent, preflight mints a new one, the probe fails, and the `ALTER ROLE` does need `CREATEROLE`; an externally managed database should therefore set both app variables explicitly. The probe only reports failure for an authentication rejection (`28P01`/`28000`); any other connection error propagates rather than being misread as a stale password. Both assignments must stay in `preflightChecks.ts`, because `db/poolManager.ts` freezes its credentials at module load\n- `BETTER_AUTH_SECRET` is mandatory. It signs session cookies and encrypts stored 2FA/TOTP secrets, so a value that changes between restarts logs every user out and permanently locks out anyone with 2FA enabled. Startup used to mint a throwaway one when it was missing, which made exactly that happen silently; it now fails preflight instead\n- Preflight also refuses to start when `BETTER_AUTH_SECRET` or `SPARKY_FITNESS_API_ENCRYPTION_KEY` still holds a template placeholder (a value starting with `changeme` or `replace_with`, as shipped in `docker/.env.example` and `docker/.env.simple.example`), and only warns for `SPARKY_FITNESS_DB_PASSWORD` because Compose initialises Postgres with it. Keep new template secrets on one of those prefixes so the check covers them. It also fails when `BETTER_AUTH_SECRET` base64-decodes to an empty key (Better Auth accepts an empty Buffer) and warns under 32 decoded bytes\n- Common operational toggles include `SPARKY_FITNESS_SERVER_PORT`, `SPARKY_FITNESS_ADMIN_EMAIL`, `ALLOW_PRIVATE_NETWORK_CORS`, `ALLOW_PRIVATE_NETWORK_AI`, `ALLOW_PRIVATE_NETWORK_FOOD_PROVIDERS`, `SPARKY_FITNESS_EXTRA_TRUSTED_ORIGINS`, and `BETTER_AUTH_URL`\n- User-configured self-hosted food providers (Mealie/Tandoor/Norish) can point `base_url` at a private/internal address only for admins by default; a non-admin on a multi-user server is blocked unless the operator opts in, either with the admin `allow_private_network_food_providers` toggle (Admin > Global Provider Settings) or `ALLOW_PRIVATE_NETWORK_FOOD_PROVIDERS=true`. This mirrors the AI policy (a single-user self-host is an admin, so their LAN recipe server works with no config). Enforced by `utils/outboundUrlPolicy.ts` at provider save time in `services/externalProviderService.ts`. Separate from the AI toggle by design\n- The admin `allow_private_network_ai` toggle (Admin > Global AI Settings), or `ALLOW_PRIVATE_NETWORK_AI=true`, lets non-admin users use custom AI service URLs (`custom`/`ollama`/`openai_compatible`) that resolve to private/internal addresses; default off is an SSRF guard enforced by `utils/outboundUrlPolicy.ts` at save/test time and again in the runtime guarded fetch path. Current admins and global admin-created AI settings can use private URLs for self-hosted providers like Ollama\n- **Call `resolveAiNetworkPolicy` / `resolveFoodProviderNetworkPolicy`, not the `derive*` forms.** The sync `derive*` functions only see the env var; the async `resolve*` wrappers also consult the admin toggle (and only hit the database when the sync answer would be a denial). The `derive*` exports stay for unit tests and for the resolvers themselves\n\n### TypeScript and Module Conventions\n\n- This package is now almost entirely TypeScript; new source files should be `.ts`\n- Keep local relative imports using `.js` extensions from TypeScript files, for example `import foo from './foo.js'`\n- `eslint.config.js` enforces file extensions in imports\n- `tsconfig.json` uses `NodeNext`, `noEmit`, and `allowJs: false`\n- `@workspace/shared` resolves directly to `../shared/src/index.ts` here and in Vitest\n- Avoid using `any` declarations in models, repositories, and integration services (e.g. `integrations/fatsecret/fatsecretService.ts`). Instead, use base datatypes (like `string`), proper types/interfaces, or import strict type schemas directly from `@workspace/shared`.\n- New public endpoints should include TypeScript code, Zod validation, and automated tests\n\n### Logging\n\n- Use `log(level, message, ...args)` from `config/logging.ts`; levels are `'debug'`, `'info'`, `'warn'`, and `'error'`\n- Never use `console.error` (or other `console.*`) in application code\n- Exception: fatal boot diagnostics in `index.ts` and `utils/preflightChecks.ts` print with `console.error` (alongside `log('error', ...)`), because `log()` is suppressed at `SILENT` and the operator must still see why the server refused to start\n- `SPARKY_FITNESS_LOG_LEVEL` controls verbosity (`DEBUG`, `INFO`, `WARN`, `ERROR`, `SILENT`)\n\n### Database and RLS\n\n- Use `getClient(userId, authenticatedUserId?)` from `db/poolManager.ts` for normal user-scoped queries\n- `getClient(...)` sets `public.set_app_context(...)`; that is what makes row-level security work correctly\n- Use `getSystemClient()` only for admin, migration, startup, or policy-management work that intentionally bypasses RLS\n- Always release database clients in a `finally` block\n- To learn a table's current shape, read `../shared/src/schemas/database/<Table>.zod.ts` (one small Zod file per table) instead of reading `../db_schema_backup.sql` or reconstructing it from the 185 migration files\n- New migrations belong in `db/migrations/` and must use `YYYYMMDDHHMMSS_description.sql`\n- **Never manually edit `../db_schema_backup.sql`** — after merge, CI regenerates it from the migrations and opens an automated sync PR (`.github/workflows/schema-backup.yml`). Do not commit copies generated from a local database.\n- If you add a new table or change user-visible access behavior, follow `../agent-docs/new-migration-checklist.md`. In short, you MUST:\n  1. Add/modify the RLS policies in `db/rls_policies.sql`.\n  2. Update the user-facing documentation in `../docs/src/features/family-friends-sharing.md`.\n  3. Update the developer-facing documentation in `../docs/src/developer/database-security-tiers.md` to define its security tier (Tier 1, Tier 2, or Tier 3).\n  4. Add or update the matching Zod schema in `../shared/src/schemas/database/`.\n- Keep future schema-startup steps in `utils/initializeDatabase.ts` and pass its shared client through all database work. The lock and schema work must use the same connection so initialization cannot continue on another connection after the lock-owning session is lost. Do not create alternate migration mechanisms.\n- Migrations run from `index.ts`, **before any application module is imported**, and via dynamic `await import()`. Both details are load-bearing: Better Auth validates the schema eagerly at `auth.ts` module scope and caches a mismatch for the life of the process (issues #2469 / #2470), and `db/poolManager.ts` builds its pools at module load, so a static import would be hoisted above the env/secret loading. `tests/bootOrder.test.ts` guards this\n\n### Uploads: Public vs Sensitive\n\n- `SparkyFitnessServer.ts` serves the uploads root publicly at `/uploads` and `/api/uploads`; both are in `publicRoutes`, so `authenticate` never runs on them\n- Sensitive subtrees are **denied on the static mount** and served instead by an authenticated, owner-checked per-id route. Three exist today:\n  - `check-in` -> `GET /api/measurements/check-in-photos/file/:id` (delegatable via the `checkin` permission)\n  - `pregnancy` -> `GET /api/v2/pregnancy/photos/file/:id` (owner-only; deliberately **no** `checkPermissionMiddleware`, because reproductive-health data is never delegated)\n  - `symptoms` -> `GET /api/v2/symptoms/photos/file/:id` (delegatable via the `symptoms` permission)\n- Adding a sensitive upload subtree means adding its directory name to `SENSITIVE_UPLOAD_SUBTREES` in `SparkyFitnessServer.ts` **and** adding an authenticated file route; the deny rule matches the decoded, normalized path, because a prefix match on the raw URL is bypassable with `..%2f`\n- Responses for these domains omit `file_path`: the on-disk layout is a server detail and clients address photos by id\n- `tests/uploadsStaticMount.test.ts` guards both the deny behavior and the fact that the deny rule is registered before `express.static`\n\n### Auth and Request Context\n\n- Better Auth is configured in `auth.ts` and mounted under `/api/auth`\n- `SparkyFitnessServer.ts` intercepts `/api/auth*` requests before the normal request logger and has special handling for discovery routes and sign-out cookie cleanup\n- `middleware/authMiddleware.ts` populates:\n  - `req.userId`\n  - `req.authenticatedUserId`\n  - `req.originalUserId`\n  - `req.activeUserId`\n  - `req.user`\n- `req.userId` is the active RLS target; `req.authenticatedUserId` is the logged-in actor\n- Family and delegated access flow through `middleware/checkPermissionMiddleware.ts`, `middleware/onBehalfOfMiddleware.ts`, and the auth middleware’s active-user switching\n- `checkPermissionMiddleware(permissionType)` guards routes; permission types are `'diary'`, `'reports'`, `'checkin'`, `'medications'`, and `'symptoms'` (GET resolves to the read-only `*_read` variant)\n- If you change auth behavior, check both cookie-backed sessions and API key flows\n- API keys carry a scope in `api_key.permissions` (Better Auth format, `@workspace/shared` `ApiKeys.api.zod.ts`): `{ sparky: [\"read\"] }` is read-only, `{ sparky: [\"read\", \"write\"] }` or no permissions is full access. The scope is set only by `POST /api/identity/user/generate-api-key`, because Better Auth refuses `permissions` from a client. `authenticate` sets `req.apiKeyReadOnly` and answers 403 for anything but GET/HEAD/OPTIONS plus the POST allowlist in `utils/apiKeyScope.ts`; the `/api/auth` interceptor refuses mutations from read-only keys via `middleware/readOnlyApiKeyGuard.ts`; `/mcp` filters tools and actions with `ai/mcp/toolAccess.ts`. A new MCP tool or action is unavailable to read-only keys until it is classified there\n\n### Dates, Day Strings, and Timezones\n\n- Prefer the shared helpers exported by `@workspace/shared` for day-string and timezone-aware logic\n- Common server-side helpers include `todayInZone`, `instantToDay`, `instantForDay`, `dayToUtcRange`, `dayRangeToUtcRange`, `localDateToDay`, `addDays`, `compareDays`, and `isDayString`\n- Load the user timezone through `utils/timezoneLoader.ts` before deriving \"today\", bucketing events by day, or building date ranges from user context\n- Treat `YYYY-MM-DD` values as calendar-day strings, not UTC-midnight timestamps\n- Avoid `toISOString().split('T')[0]` for user-facing or business-logic dates; it silently shifts dates near timezone boundaries\n- If you touch older code that still uses UTC split patterns, prefer migrating that path to the shared helpers instead of copying the pattern forward\n- Timezone/date regression coverage already exists in:\n  - `tests/timezone.test.ts`\n  - `tests/dateShifting.test.ts`\n  - `tests/measurementService.timezone.test.ts`\n\n### Integrations and Background Work\n\n- Provider-specific adapters live under `integrations/`; coordinating logic usually lives in `services/` and persistence in `models/`\n- Current adapters span food/nutrition (OpenFoodFacts, FatSecret, Nutritionix, USDA, Mealie, Tandoor, Norish, SwissFood, Yazio), fitness devices (Garmin Connect sync plus FIT file import via `integrations/garminfit/` + `services/fitImportService.ts`, Withings, Fitbit, Oura, Polar, Strava, COROS, Hevy), exercise databases (Wger, FreeExerciseDB), and health-data import (Google Health, generic/mobile health data)\n- Scheduled jobs currently include backups, session cleanup, and hourly sync loops for Withings, Garmin, Fitbit, Oura, Polar, Strava, and COROS\n- Integration work often spans route, service, repository, cron, and external-provider settings code; inspect the whole path before calling the work complete\n- **OAuth linking (`/authorize`, `/callback`) is self-only, and `state` is a server-issued single-use nonce.** Never derive a user id from a callback request body, and never gate an authorize route with `checkPermissionMiddleware('diary')` — on GET that resolves to `diary_read`, which would hand a read-only delegate the owner's decrypted OAuth client id. Use `requireSelfActor` plus `utils/oauthState.ts`. Withings and Polar follow this pattern; Oura, Fitbit and Strava are self-only but still send `state = userId` and ignore it on callback (tracked follow-up)\n\n### AI Services\n\n- AI calls go through the Vercel `ai` SDK (v7) with provider adapters for OpenAI, Anthropic, and Google, plus OpenAI-compatible, Mistral, Groq, OpenRouter, and Ollama service types\n- `ai/config.ts` holds default model and vision-model selection per provider; `ai/providerDispatch.ts` is the unified dispatch helper used by chat, food-photo analysis, nutrition-label scan, and unit conversion\n- Prefer routing new AI features through `providerDispatch.ts` instead of calling provider SDKs directly\n- Web and mobile voice transcription use `POST /api/chat/transcribe` in `routes/chatRoutes.ts` and `services/voiceTranscriptionService.ts`; its multipart fields, response, MIME types, and 10 MB limit come from `shared/src/schemas/api/VoiceTranscription.api.zod.ts`. Audio stays in memory, uses actor-scoped AI settings with `supports_audio_input`, and all outbound calls use `createGuardedFetch`. Voice and chat providers are selected independently; transcription never executes chatbot tools or writes diary entries. `voice_input_enabled` and `active_voice_ai_service_id` in user preferences synchronize voice UI across clients; null selects System recognition, and disabling voice preserves the selection. Preference writes validate these fields using the shared voice-preferences schema.\n- Chatbot tool calls run in-process through the registry in `ai/tools/`\n- `ai/tools/index.ts` exposes `buildChatbotTools(userId, tz)`, composing the per-domain builders (`build<Domain>Tools` in `ai/tools/<domain>Tools.ts`); handlers close over the authenticated user — so two-actor services receive `(userId, userId, ...)` — and the user's IANA timezone, used for \"today\" defaults and day bucketing\n- Tool handlers follow a fixed contract: publish a flat Zod schema, validate with a strict union `safeParse` inside `execute`, orchestrate through existing services and repositories, and never throw - errors come back as `ERRORS.*` strings from `ai/tools/errors.ts`\n- Tool output text is a parity contract with the MCP tool set; golden tests in `tests/chatbotTools*.test.ts` assert exact returned strings, so do not reword tool output casually\n\n## Testing and Validation\n\n- Test runner: Vitest, not Jest\n- Auto-discovered test files match `tests/**/*.test.ts`\n- `tests/check_routes.ts` and `tests/*.script.ts` are utility scripts, not normal test suites\n- For route or contract work, targeted `supertest`-based Vitest tests are the normal validation path\n- Prefer `pnpm run typecheck` after touching `routes/v2/`, `schemas/`, `types/`, or shared request/response contracts\n- Prefer `pnpm run lint` after multi-file edits; if unrelated package-wide issues make that noisy, run targeted `pnpm exec eslint <paths>` on the touched files before stopping\n- Use `pnpm run test:coverage` after broad service, route, repository, middleware, or auth refactors\n\n## Quick Routing\n\n- Weekly weight-change calorie goals: `services/preferenceService.ts` validates the signed `goal_mode_weekly_change_kg` preference (negative loses, positive gains; active only for `goal_mode = 'weekly'`), `models/preferenceRepository.ts` persists it, and `services/goalService.ts` plus `services/calorieBalanceService.ts` pass it to the shared calorie adjustment functions. Device Projection applies the daily weekly-rate adjustment once to device TDEE; percentage goals keep their existing behavior.\n\n- Startup, env, or deployment issue:\n  inspect `index.ts`, `SparkyFitnessServer.ts`, `utils/secretLoader.ts`, `utils/preflightChecks.ts`, and `config/logging.ts`\n- Auth, session, MFA, or API key issue:\n  inspect `auth.ts`, `middleware/authMiddleware.ts`, `routes/authRoutes.ts`, and `routes/auth/`\n- Migration, RLS, or permission issue:\n  inspect `db/migrations/`, `db/rls_policies.sql`, `db/poolManager.ts`, `utils/applyRlsPolicies.ts`, and the permission middleware/helpers\n- Public v2 contract issue:\n  inspect the matching file in `routes/v2/` plus the related Zod schema in `schemas/`\n- Food, barcode, or external provider issue:\n  inspect the relevant `integrations/*` code, then the matching service and repository files\n- Open Food Facts publication:\n  inspect `services/openFoodFactsManualContributionService.ts`, `integrations/openfoodfacts/openFoodFactsContribution.ts`, and `constants/openFoodFacts.ts`; retained automatic queue code is dormant and needs a new migration before a future release can activate its triggers\n- Heart rate zone, max heart rate or time-in-zone issue:\n  inspect `../shared/src/utils/heartRateZones.ts`, `services/heartRateZoneService.ts`, `routes/v2/heartRateZoneRoutes.ts`, then `services/healthDataHandlers.ts` (`persistWorkoutTelemetry`) and `services/exerciseEntryService.ts` (watch telemetry) for the write paths\n- Health data or date bucketing issue:\n  inspect `integrations/healthData/healthDataRoutes.ts`, `services/measurementService.ts`, and `utils/timezoneLoader.ts`\n- Attaching heart rate to an already-existing exercise entry (e.g. from a paired Apple Watch's live workout tracking, `SparkyFitnessMobile/src/hooks/useWatchWorkoutBridge.ts`):\n  inspect `POST /exercise-entries/:id/watch-telemetry` in `routes/exerciseEntryRoutes.ts`, `services/exerciseEntryService.ts`'s `attachWatchTelemetryToExerciseEntry`, and `models/exerciseEntry.ts`'s `applyWatchTelemetryAtomically`. Distinct from `services/healthDataHandlers.ts`'s `persistWorkoutTelemetry`, which creates a new entry as part of importing a whole synced workout (HealthKit/Health Connect/Garmin) — this route only fills in avg/max HR, `exercise_entry_hr_zones` and `calories_burned` on an entry that already exists, and it also stores the posted series as `heart_rate` samples tagged with the entry (`storeHeartRateSeries`, source `SparkyFitness Watch`, `merge` over the workout's own span but only this entry's samples, and only after that snapshot was accepted under the row lock, best effort) because the heart rate chart of an entry with no GPS track reads those samples and would otherwise wait on a Health sync. `activeEnergyKcal` is a real measurement from the watch and is written to BOTH `calories_burned` (the figure the diary totals) and `active_calories` (a telemetry column the ordinary entry update preserves). That second write is what makes the measurement survive a later edit: `resolveEditedCaloriesBurned` in `services/exerciseService.ts` prefers a stored `active_calories` over the recomputed duration-and-sets estimate, so editing a note or a weight no longer replaces what a watch measured with a formula. A client-sent `calories_burned` still overrides both. Both body fields are individually optional (HealthKit permissions are per type) and the model does a partial UPDATE, so a calories-only post must not blank heart rate an earlier post attached.\n  Workout effort (a 1-10 rating of the whole workout, `exercise_entries.workout_effort`, CHECK 1-10): it is one of `EXERCISE_ENTRY_TELEMETRY_COLUMNS`, so a synced workout carries it as `telemetry.workout_effort` through the health-data allowlist (HealthKit sends the person's own Apple rating) and the model's insert, update and select pick it up with no per-column code. `POST /exercise-entries/:id/effort` (`setExerciseEntryEffort`, body `{rating}`) is the write for ratings given afterwards, on the watch: it sets the entry and every other entry with the same `exercise_preset_entry_id`, so a strength session reads the same from any exercise and the phone needs only one entry id. `GET /api/exercise-stats/query` returns it as `workoutEffort`.\n- Attaching a phone-recorded GPS track to an existing exercise entry (mobile `RecordActivityScreen`):\n  inspect `POST /exercise-entries/:id/gps-track` in `routes/exerciseEntryRoutes.ts` and `services/exerciseEntryService.ts`'s `attachGpsTrackToExerciseEntry`, with the request schema `attachExerciseEntryGpsTrackRequestSchema` in `shared/src/schemas/api/ExerciseEntries.api.zod.ts`. It stores the route and laps (laps arrive as bare windows and are derived by `deriveLaps`) and backfills speed/elevation/moving-time with `deriveWorkoutTelemetry`, all in one transaction. Both inserts upsert per entry, so a retried post replaces the track. Use this rather than `/api/health-data` for app-recorded activities: the health-data path range-deletes a source's entries for the affected days.\n- Attaching laps to an entry with no GPS track (an indoor run recorded on the watch):\n  inspect `POST /exercise-entries/:id/laps` in `routes/exerciseEntryRoutes.ts` and `services/exerciseEntryService.ts`'s `attachLapsToExerciseEntry`, with `attachExerciseEntryLapsRequestSchema` in `shared/src/schemas/api/ExerciseEntries.api.zod.ts`. The client sends the marked windows with the distance and average heart rate it worked out; the server derives duration and speed and upserts by lap index into `exercise_entry_laps`, so a retry replaces them. Entries with a track get their laps from `/gps-track` instead.\n  For an indoor run's pace chart, `POST`/`GET /exercise-entries/:id/distance-series` (`attachDistanceSeriesToExerciseEntry`, `getExerciseEntryDistanceSeries`, table `exercise_entry_distance_series`, rows of `[active seconds, metres]`; `setExerciseEntryDistance` rescales them with the laps).\n- Correcting the distance of an entry with no GPS track (a treadmill run, from the summary screen):\n  inspect `POST /exercise-entries/:id/distance` in `routes/exerciseEntryRoutes.ts`, `services/exerciseEntryService.ts`'s `setExerciseEntryDistance` and `setExerciseEntryDistanceRequestSchema` in `shared/src/schemas/api/ExerciseEntries.api.zod.ts`. It changes only the entry's `distance`, its sets' distance and any laps (`distance_meters` and speed scaled by new over old distance), in one transaction, so measured heart rate and energy are kept; an entry with a GPS track is refused with 409.\n- Water, hydration, caffeine, or alcohol issue:\n  inspect `services/hydrationTotalsService.ts` (the single owner of the daily water formula), `services/measurementService.ts` (the container \"+/-\" path and the container->food link), `services/caffeineKineticsService.ts` / `services/alcoholWeekService.ts`, `models/waterContainerRepository.ts`, and the shared maths in `../shared/src/nutrients/`\n- Self-service \"delete synced data by source\" issue:\n  inspect `routes/syncedDataRoutes.ts`, `services/syncedDataService.ts`, and `models/syncedDataRepository.ts` (the `SYNCED_SOURCE_TABLES` whitelist)\n- AI chat or chatbot tool issue:\n  inspect `services/chatService.ts`, `ai/tools/`, and the matching domain service and repository\n- Fasting or mood issue:\n  inspect `routes/fastingRoutes.ts` / `routes/moodRoutes.ts` and `models/fastingRepository.ts` / `models/moodRepository.ts`\n  `mood_entries` holds several rows per day, each with `entry_timestamp` and a `source`: `manual` (web/app check-ins, AI `log_mood`, end-of-fast mood), or `garmin` / `import` (derived, one row per source per day via a partial unique index). `mood_value` is nullable; a CHECK needs at least one of mood, energy, focus, notes or tags (whitespace-only notes and blank tags do not count, as in `hasMoodContent`), and another limits `source` to those three values. Requests without the `X-Mood-Model-Version` header keep the one-mood-per-day contract on the \"legacy row\" (latest manual row with a mood, falling back to the derived row on GET, Garmin before import); `X-Mood-Model-Version: 2` always adds a check-in and reads arrays (header is in `CORS_ALLOWED_HEADERS`, `utils/corsHelper.ts`); its PUT edits only manual rows (409 for Garmin and import) and re-dates a check-in only when its timestamp actually changes. A check-in or derived mood written without a time of its own gets `instantForDay` from `@workspace/shared`: now for today, local noon for another day, so `entry_timestamp` stays on `entry_date`. Per-day aggregates (coach, cycle correlations) use `DAILY_MOOD_VALUE_SQL`: rounded average of manual moods, else Garmin, else import.\n- Symptom or episode tracking issue (#1882):\n  inspect `routes/v2/symptomRoutes.ts`, `services/symptomService.ts`, `models/symptomRepository.ts`, `models/symptomOptionRepository.ts`, and the shared contract and built-ins in `../shared/src/schemas/api/Symptoms.api.zod.ts` and `../shared/src/symptoms/`. `symptom_entries` also holds cycle-hub symptoms (`source = 'cycle'`), which RLS keeps owner-only\n- Medications, cycle, or pregnancy issue:\n  inspect the matching v2 route (`routes/v2/medicationRoutes.ts`, `routes/v2/cycleRoutes.ts`, `routes/v2/pregnancyRoutes.ts`), its Zod schema in `schemas/`, then `services/cycleService.ts` / `services/pregnancyService.ts` and the `models/medication*Repository.ts` / `models/cycleRepository.ts` / `models/pregnancyRepository.ts` files\n- Supplement barcode lookup:\n  `GET /api/v2/medications/supplement-lookup?upc=` (registered before `/:id`) runs only while the user can see an active `dsld` or `openfoodfacts` external provider (`getActiveProvidersByTypes`; neither is a 404; the NIH database answers first and `services/supplementOpenFoodFactsService.ts` asks Open Food Facts, per-serving nutrients only, when it has no match or `dsld` is off; migration `20261006200000_add_dsld_provider_type.sql`, constant `SUPPLEMENT_LOOKUP_PROVIDER_TYPE`) and calls `services/supplementLookupService.ts`. Two more routes read a Supplement Facts photo: `POST /supplement-label/scan` runs the user's vision AI (`services/supplementLabelScanService.ts`, same provider and error map as `/foods/scan-label`) and `POST /supplement-label/map` takes ingredients the phone read on device; both end in `mapScannedLabel`, so name matching and unit conversion are shared with the barcode lookup, which searches the NIH Dietary Supplement Label Database for the code, opens the hits to check the label's own UPC, and maps the label to one serving's nutrients in the app's units (IU for vitamins A, D and E converted, compounds and unplaceable ingredients returned as `unmatched`). The response contract is `shared/src/schemas/api/SupplementLookup.api.zod.ts`; an unreachable database is a 502, no match is `{ product: null }`.\n- Exercise alternatives, workout feedback, or adaptive suggestions issue (#1560):\n  inspect `services/exerciseAlternativesService.ts` (library + Free Exercise DB candidates, dedupe) with the pure ranking in `utils/exerciseAlternativesRanking.ts` and the muscle/equipment vocabulary in `../shared/src/constants/exerciseTaxonomy.ts`; feedback in `services/workoutCoachingService.ts` + `models/workoutFeedbackRepository.ts`; signals in `services/adaptiveWorkoutService.ts`. The rules that turn signals into weight changes are client-side and shared (`../shared/src/utils/adaptiveCoaching.ts`) so web, mobile and the AI tools agree; the server only reports what happened. AI actions: `suggest_alternatives`, `rate_workout`, `get_workout_coaching` in `ai/tools/exerciseTools.ts`\n- Bodyweight exercise load, volume or 1RM issue (#56):\n  a `bodyweight_reps` set's weight is signed added/assisting load, and its load is body weight plus that weight (`effectiveLoadKg` in `../shared/src/utils/exerciseLoad.ts`). SQL mirrors the rule in `utils/exerciseLoadSql.ts` (`bodyWeightJoinSql`, `setLoadSql`), used by `services/exerciseStatsService.ts` and `models/exerciseEntry.ts`'s progress query; `services/reportService.ts` applies the shared helper with readings from `reportRepository.getBodyWeightReadings`. Keep the two copies identical. A negative set weight is rejected unless that exercise's modality is `bodyweight_reps`. `models/exercise.ts`'s `createExercise` defaults the modality from equipment through `resolveExerciseModality`\n  `weight_distance` (weighted carries) and `weight_duration` (loaded holds) store weight plus distance (km) or duration (s); stats treat them as weight-based, not cardio, and best-set ordering falls back to distance then duration.\n- Sleep or sleep-science issue:\n  inspect `routes/sleepRoutes.ts`, `routes/sleepScienceRoutes.ts`, `services/sleepAnalyticsService.ts`, `services/sleepScienceService.ts`, and the sleep repositories\n\n## Architecture Resources\n\nBefore adding a feature or changing auth/permission behavior, read:\n\n- `../docs/src/developer/database.md` — Quick table index (all ~120 tables with purpose) + migration best practices\n- `../docs/src/developer/database-security-tiers.md` — Security tier, permission type, and RLS rules for every table (authoritative)\n- `../agent-docs/architecture-permissions.md` — Permission types, links to tier classification doc\n- `../agent-docs/data-flow-patterns.md` — Data flow from frontend through server to database, safe RLS patterns\n- `../agent-docs/new-domain-template.md` — Checklist for adding a major feature domain\n- `../agent-docs/anti-patterns.md` — Common mistakes (using getSystemClient(), forgetting RLS, cache invalidation, timezone bugs, cross-package contract mismatches)\n\n## Working Rules\n\n- Match the existing service/repository/middleware layering instead of introducing parallel abstractions\n- **Library Deletes vs Diary Snapshots:** `exercise_entries` and `food_entries` are snapshot-backed (`exercise_id` / `food_id` are `ON DELETE SET NULL`). `deleteExercise` and `deleteFood` (`mode: 'delete'`) must never delete past or today's diary entries; they cascade from templates/presets, clean up future scheduled plan entries (`entry_date >= today AND workout_plan_assignment_id IS NOT NULL`), and clean up empty parent preset entries. Only explicit `delete_with_history` (force delete) deletes diary entries for that user. If an item is referenced by others (`otherUserReferences > 0`), the delete must fall back to `hide` (`is_quick_exercise` / `is_quick_food`).\n- If your change adds a new domain, route family, or table, update this file's Snapshot, Source Map, and Quick Routing sections (and the `Last updated` date) in the same change\n- If you add persisted or user-visible data, think through migration, RLS, permissions, tests, API docs, and downstream client contracts together\n- Validate shared-contract","isInternal":false,"tokens":11941,"sizeBytes":50034},{"name":"SKILL.md","path":".claude/skills/new-migration/SKILL.md","rawUrl":"https://raw.githubusercontent.com/CodeWithCJ/SparkyFitness/HEAD/.claude/skills/new-migration/SKILL.md","title":"new-migration","category":"anthropic-skill","format":"markdown","content":"---\nname: new-migration\ndescription: Use whenever adding or changing a SparkyFitness database migration, creating a new table, or altering user-visible data access. Walks the mandatory cross-package checklist (RLS policies, schema backup sync, shared Zod schema, docs security tiers, downstream contracts). Triggers on \"new table\", \"migration\", \"ALTER TABLE\", \"RLS\", \"schema change\".\n---\n\n# New Migration\n\nSee @.agents/skills/new-migration/SKILL.md — that is the canonical skill body. Read it and follow it.\n","frontmatter":{"name":"new-migration","description":"Use whenever adding or changing a SparkyFitness database migration, creating a new table, or altering user-visible data access. Walks the mandatory cross-package checklist (RLS policies, schema backup sync, shared Zod schema, docs security tiers, downstream contracts). Triggers on \"new table\", \"migration\", \"ALTER TABLE\", \"RLS\", \"schema change\"."},"isInternal":false,"tokens":115,"sizeBytes":510},{"name":"CLAUDE.md","path":"CLAUDE.md","rawUrl":"https://raw.githubusercontent.com/CodeWithCJ/SparkyFitness/HEAD/CLAUDE.md","title":"Claude Agent Guidelines & System Prompt","category":"claude-rule","format":"markdown","content":"See @AGENTS.md\n","isInternal":false,"tokens":6,"sizeBytes":15},{"name":"CLAUDE.md","path":"docs/CLAUDE.md","rawUrl":"https://raw.githubusercontent.com/CodeWithCJ/SparkyFitness/HEAD/docs/CLAUDE.md","title":"docs - Claude Agent Guidelines & System Prompt","category":"claude-rule","format":"markdown","content":"# CLAUDE.md\n\nThis is the SparkyFitness documentation site built with **VitePress**. For work on docs, inspect `package.json` scripts and the `src/` layout directly (`.vitepress/config.mts`).\n","isInternal":false,"tokens":51,"sizeBytes":191},{"name":"CLAUDE.md","path":"shared/CLAUDE.md","rawUrl":"https://raw.githubusercontent.com/CodeWithCJ/SparkyFitness/HEAD/shared/CLAUDE.md","title":"shared - Claude Agent Guidelines & System Prompt","category":"claude-rule","format":"markdown","content":"# CLAUDE.md\n\nSee @AGENTS.md\n","isInternal":false,"tokens":12,"sizeBytes":28},{"name":"CLAUDE.md","path":"SparkyFitnessFrontend/CLAUDE.md","rawUrl":"https://raw.githubusercontent.com/CodeWithCJ/SparkyFitness/HEAD/SparkyFitnessFrontend/CLAUDE.md","title":"SparkyFitnessFrontend - Claude Agent Guidelines & System Prompt","category":"claude-rule","format":"markdown","content":"# CLAUDE.md\n\nSee @AGENTS.md\n","isInternal":false,"tokens":12,"sizeBytes":28},{"name":"CLAUDE.md","path":"SparkyFitnessMobile/CLAUDE.md","rawUrl":"https://raw.githubusercontent.com/CodeWithCJ/SparkyFitness/HEAD/SparkyFitnessMobile/CLAUDE.md","title":"SparkyFitnessMobile - Claude Agent Guidelines & System Prompt","category":"claude-rule","format":"markdown","content":"See @AGENTS.md\n","isInternal":false,"tokens":6,"sizeBytes":15},{"name":"CLAUDE.md","path":"SparkyFitnessServer/CLAUDE.md","rawUrl":"https://raw.githubusercontent.com/CodeWithCJ/SparkyFitness/HEAD/SparkyFitnessServer/CLAUDE.md","title":"SparkyFitnessServer - Claude Agent Guidelines & System Prompt","category":"claude-rule","format":"markdown","content":"# SparkyFitnessServer - Claude Agent Guidelines & System Prompt\n\nPath: `SparkyFitnessServer/CLAUDE.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/CodeWithCJ/SparkyFitness/HEAD/SparkyFitnessServer/CLAUDE.md)","tokens":300,"sizeBytes":227},{"name":"copilot-instructions.md","path":".github/copilot-instructions.md","rawUrl":"https://raw.githubusercontent.com/CodeWithCJ/SparkyFitness/HEAD/.github/copilot-instructions.md","title":"GitHub Copilot Instructions","category":"copilot-instructions","format":"markdown","content":"# SparkyFitness AI Coding Agent Instructions\n\n## Architecture Overview\n\n**Full-stack fitness tracking application** - self-hosted alternative to MyFitnessPal with AI-powered nutrition assistance.\n\n- **Frontend**: React 18 + TypeScript + Vite (`src/`)\n- **Backend**: Node.js/Express (`SparkyFitnessServer/`)\n- **Database**: PostgreSQL with Row Level Security (RLS)\n- **UI**: Tailwind CSS + shadcn/ui components\n- **State**: React Context + TanStack Query\n- **AI**: Multi-provider support (OpenAI, Anthropic, Google, etc.)\n\n## Development Workflow\n\n### Running the Application\n```bash\n# Frontend development server (port 8080)\nnpm run dev\n\n# Backend server (port 3010)  \nnpm run start-backend\n\n# Docker development (local builds)\n./docker/docker-helper.sh dev up\n\n# Docker production (DockerHub images)\n./docker/docker-helper.sh prod up\n```\n\n### Key Configuration Files\n- **Vite config** (`vite.config.ts`): API proxy setup (`/api` → `localhost:3010`)\n- **Environment**: Root `.env` file (loaded by backend at `path.resolve(__dirname, '../.env')`)\n- **Docker**: `docker/` directory contains organized Docker configuration\n  - `docker-compose.dev.yml` - Local development builds with volume mounts\n  - `docker-compose.prod.yml` - Production DockerHub images\n  - `docker-helper.sh` - Comprehensive management script with help system\n  - `nginx.conf.template` - Parameterized nginx configuration with envsubst\n  - `.env.example` - Template for environment variables\n\n### Docker Helper Script\nThe `docker-helper.sh` script provides easy management:\n```bash\n# Show all available commands\n./docker/docker-helper.sh help\n\n# Development environment\n./docker/docker-helper.sh dev up     # Start all services\n./docker/docker-helper.sh dev logs   # View logs\n./docker/docker-helper.sh dev clean  # Clean volumes and images\n\n# Production environment  \n./docker/docker-helper.sh prod up    # Start production services\n./docker/docker-helper.sh prod down  # Stop and remove containers\n```\n\n## Critical Architecture Patterns\n\n### Repository Pattern (Backend)\nAll database operations use repository pattern in `SparkyFitnessServer/models/`:\n```javascript\n// Example: userRepository.js\nconst pool = require('../db/connection');\n\nasync function createUser(userId, email, hashedPassword, full_name) {\n  const client = await pool.connect();\n  try {\n    await client.query('BEGIN');\n    // Multiple related inserts in transaction\n    await client.query('COMMIT');\n  } catch (error) {\n    await client.query('ROLLBACK');\n    throw error;\n  } finally {\n    client.release();\n  }\n}\n```\n\n### External Provider Integration\nModular integration system in `SparkyFitnessServer/integrations/`:\n- **Food providers**: OpenFoodFacts, Nutritionix, FatSecret\n- **Exercise data**: Wger integration\n- **Health data**: Apple Health integration\n- **Encryption**: All API keys encrypted at rest using `security/encryption.js`\n\n### Frontend Context Providers\nKey contexts in `src/contexts/`:\n- **PreferencesContext**: User settings, goals, theme, AI provider config\n- **ChatbotVisibilityContext**: AI assistant state management\n\n### Database Schema Conventions\n- **UUID primary keys** for all tables (`gen_random_uuid()`)\n- **Audit fields**: `created_at`, `updated_at` on all tables\n- **RLS policies**: Users only access their own data\n- **Transaction patterns**: Multi-table operations wrapped in transactions\n\n## AI Integration Patterns\n\n### Multi-Provider AI Support\n```javascript\n// SparkyFitnessServer/ai/config.js\nfunction getDefaultModel(serviceType) {\n  switch (serviceType) {\n    case 'openai': return 'gpt-4o-mini';\n    case 'anthropic': return 'claude-3-5-sonnet-20241022';\n    case 'google': return 'gemini-pro';\n    // ...\n  }\n}\n```\n\n### Chat System Architecture\n- **Backend**: `routes/chatRoutes.js` handles AI provider routing\n- **Frontend**: `DraggableChatbotButton` component for UI\n- **Storage**: Chat history with metadata in PostgreSQL\n- **Capabilities**: Food logging, image analysis, exercise tracking, measurements\n\n## Data Flow Patterns\n\n### Meal Planning System\n1. **Templates** (`meal_plan_templates`) → **Scheduled Plans** (`meal_plans`) → **Food Diary Entries** (`food_diary`)\n2. Complex meal expansion logic in `mealPlanTemplateRepository.js`\n3. Date-range planning support with single-day granularity\n\n### Family Access Control\n- **Granular permissions**: `calorie`, `checkin`, `reports`, `food_list`\n- **RLS enforcement**: Database-level security policies\n- **Access patterns**: `familyAccessRepository.js` manages relationship permissions\n\n### External API Error Handling\nConsistent error patterns across integrations:\n```javascript\ntry {\n  const response = await fetch(searchUrl, { method: 'GET' });\n  if (!response.ok) {\n    const errorText = await response.text();\n    log('error', \"API error:\", errorText);\n    throw new Error(`API error: ${errorText}`);\n  }\n  return await response.json();\n} catch (error) {\n  log('error', `Error with query \"${query}\":`, error);\n  throw error;\n}\n```\n\n## Development Conventions\n\n### API Response Patterns\n- **Success**: JSON data directly\n- **Error**: `{ error: \"message\" }` with appropriate HTTP status\n- **Auth**: JWT tokens in Authorization header\n- **CORS**: Configured for frontend URL in environment\n\n### Database Migration Strategy\n- **Auto-migrations**: `utils/dbMigrations.js` runs on server startup\n- **Version tracking**: Migrations applied based on version numbers\n- **Schema changes**: Always backward compatible during development\n\n### Component Architecture\n- **shadcn/ui** for base components\n- **Custom components** in `src/components/`\n- **Page-level** components in `src/pages/`\n- **Shared utilities** in `src/utils/`\n\n### Environment-Specific Behavior\n- **Development**: Vite proxy, hot reload, debug logging\n- **Docker**: Production builds, environment variable injection\n- **Database**: Connection pooling, transaction management\n\n## Key Files for Understanding\n- **Server entry**: `SparkyFitnessServer/SparkyFitnessServer.js` (route registration, middleware setup)\n- **Frontend entry**: `src/App.tsx` (context providers, query client setup)\n- **Documentation site**: `docs/` - Comprehensive Nuxt Content documentation (https://codewithcj.github.io/SparkyFitness)\n\n## Documentation Structure\n\n### Developer Documentation\nThe developer documentation has been consolidated into focused guides at `docs/content/3.developer/`:\n\n1. **`1.getting-started.md`** - Complete setup, installation, and quick start guide\n2. **`2.architecture.md`** - System architecture, patterns, and project structure  \n3. **`3.contributing.md`** - Development workflow, guidelines, and PR process\n4. **`4.database.md`** - Database schema, migrations, and operations\n5. **`5.api-reference.md`** - OpenAPI specification and endpoint documentation\n6. **`6.troubleshooting.md`** - Comprehensive debugging guide and common solutions\n\n### Documentation Site Features\n- **Live site**: https://codewithcj.github.io/SparkyFitness hosted on GitHub Pages\n- **Technology**: Nuxt Content + Docus theme for optimal developer experience\n- **Navigation**: Auto-generated navigation with numerical ordering\n- **Search**: Full-text search across all documentation\n- **Mobile-friendly**: Responsive design for all devices\n","isInternal":false,"tokens":1637,"sizeBytes":7232}],"systemPromptSnippet":"<yakaai_skills repo=\"CodeWithCJ/SparkyFitness\">\n<!-- File: AGENTS.md (Tokens: ~3148 | Category: root-instruction) -->\n# AGENTS.md\n\n*Last updated: 2026-10-22*\n\nThis is the repo-root monorepo guide for SparkyFitness. Use it to choose the right package, understand shared repo-level rules, and find the next guide to read.\n\n**For AI Tools & Developers:** Start with `agent-docs/README.md` (in this repo) for quick navigation to:\n- `file-and-domain-reference.md` — Find any code by feature in seconds\n- `testing-patterns.md` — Concrete test examples for each layer\n- `architecture-permissions.md` — Permission types and RLS patterns\n- Plus 5 more guides for migrations, data flow, anti-patterns, new features, and planning\n\nPackage-level guides win. For work inside a package, follow that package's `AGENTS.md` when present, otherwise its `CLAUDE.md`.\n\n## Scope\n\n- Start here when work begins at repo root or spans multiple packages.\n- Keep root-level guidance focused on workspace layout, shared conventions, and cross-package coordination.\n- Root `package.json` is tooling only (`husky`, `lint-staged`, `prettier`), not an app entrypoint.\n- Run scripts from the package directory you are changing.\n\n## Package Guides\n\n- Repo-root alias: `CLAUDE.md` points to this file.\n- Frontend: `SparkyFitnessFrontend/AGENTS.md`\n- Server: `SparkyFitnessServer/AGENTS.md`\n- Mobile: `SparkyFitnessMobile/AGENTS.md`\n- Shared: `shared/AGENTS.md`\n\nFor `docs/` and `SparkyFitnessGarmin/`, there is no package-level `AGENTS.md`. `SparkyFitnessGarmin/` is only a handful of Python files (`main.py`, `routes.py`, `service.py`, `schemas.py`); read them directly. For `docs/`, inspect the local manifest and content layout.\n\n## Monorepo Map\n\n- `SparkyFitnessFrontend/` - React 19 + Vite web app.\n- `SparkyFitnessServer/` - Express 5 + PostgreSQL backend API.\n- `SparkyFitnessMobile/` - Expo SDK 57 / React Native 0.86 app.\n- `shared/` - source-first TypeScript workspace package for `@workspace/shared` schemas, constants, and timezone/day helpers.\n- `docs/` - VitePress documentation site.\n- `docs/readme/` - translated copies of the root `README.md` (`README_<lang>.md`), outside the VitePress `srcDir`. The language bar at the top of `README.md` links to them; when the root README changes substantially, the translations go stale.\n- `SparkyFitnessGarmin/` - standalone Python integration service outside the current `pnpm` workspace.\n- `docker/`, `helm/`, `.github/` - infra and deployment assets.\n- `umbrel/sparkyfitness/` - Umbrel App Store package (manifest, compose, derived secrets), submitted to `getumbrel/umbrel-apps`. See `umbrel/README.md`.\n- `db_schema_backup.sql` - repo-root schema snapshot kept in sync by CI (`.github/workflows/schema-backup.yml`); never hand-edit or regenerate locally.\n- `docker/.env.example` - tracked env template commonly copied to repo-root `.env`.\n\n## Workspace Notes\n\n- `pnpm-workspace.yaml` currently lists `frontend`, `SparkyFitnessFrontend`, `shared`, `SparkyFitnessMobile`, `SparkyFitnessServer`, and `docs`.\n- Only `SparkyFitnessFrontend/` exists on disk right now; treat `frontend` as a legacy workspace entry unless the task is specifically about workspace cleanup.\n- `shared/` is a library package, not an app; its `pnpm run validate` runs typecheck, a hardcoded-string audit, and the Prettier check. CI runs that in a `Shared Validate` job and also re-runs the frontend, mobile, and server jobs whenever `shared/` changes, so validate shared changes from the consuming package(s), not in isolation.\n- `SparkyFitnessGarmin/` is outside the current workspace. Inspect its own manifest and scripts before working there.\n\n## Agent Efficiency (read this before searching)\n\nDo not read or search these paths; they burn context for nothing:\n\n- `WIP/` - personal scratch area; contains zips and full copies of other repos, including a stale duplicate of this repo (`WIP/SparkyFitness-main/`). Never read or edit anything under it.\n- `SparkyFitnessMobile/ios/` and `SparkyFitnessMobile/android/` - generated native projects (`ios/` is >1 GB of Pods). Regenerate with `npx expo prebuild --clean`; edit `app.config.ts`, `plugins/`, or `targets/` instead.\n- `pnpm-lock.yaml` (~1.3 MB) - never read; check `package.json` files instead.\n- `db_schema_backup.sql` (~330 KB) - never read whole; grep for the one `CREATE TABLE` you need.\n- `SparkyFitnessFrontend/dist/` - build output.\n- `SparkyFitnessFrontend/public/locales/` except `en/` - 35 machine-synced translations. Only `en/translation.json` is ever hand-edited, and even that (~120 KB) should be grepped, not read whole.\n\nCheap ways to learn things:\n\n- Database table index: read `docs/src/developer/database.md` (quick reference of all ~120 tables with one-line purpose). For detailed schema, read `shared/src/schemas/database/<Table>.zod.ts` (one small Zod file per table).\n- Database security & permissions: `docs/src/developer/database-security-tiers.md` (security tier, permission type, and RLS rules for every table).\n- API request/response contract: `shared/src/schemas/api/<Name>.api.zod.ts`.\n- Definition of done: CI (`.github/workflows/ci-tests.yml`) runs `pnpm run validate` (which includes Knip unused export & dead code checks in frontend and mobile) plus the package's CI test script for each changed package. Run those locally before declaring work complete.\n\n## Cross-Package Rules\n\n- If you add or change a server migration (such as creating a new table), follow `agent-docs/new-migration-checklist.md`. In short, you MUST:\n  1. Create the migration file in `SparkyFitnessServer/db/migrations/YYYYMMDDHHMMSS_description.sql`.\n  2. Update the Row-Level Security (RLS) policies in `SparkyFitnessServer/db/rls_policies.sql`.\n  3. **Restart the server** (`pnpm start` from `SparkyFitnessServer/`) to apply the migration.\n  4. Leave `db_schema_backup.sql` alone — after merge, CI regenerates it from the migrations and opens an automated sync PR (`.github/workflows/schema-backup.yml`). Never manually edit the backup file or commit a locally generated copy.\n  5. Add or update the matching Zod schema in `shared/src/schemas/database/`.\n  6. Update the user-facing documentation in `docs/src/features/family-friends-sharing.md`.\n  7. Update the developer documentation in `docs/src/developer/database-security-tiers.md` to classify the table as Tier 1, Tier 2, or Tier 3.\n- **Heart rate zones have one source of truth:** `shared/src/utils/heartRateZones.ts` (`resolveHeartRateZones`, `heartRateZoneRange`, `computeTimeInZones`). Never re-derive max heart rate or zone floors in a package; server stored zones, training load, mobile suggested ranges and the web/mobile charts and settings all call it. Max heart rate is the person's own value, else Nes (211 - 0.64 x age) from date of birth, else 190. Settings never rewrite stored `exercise_entry_hr_zones` rows. iOS 27 can fill custom floors from Apple Health (`floorsFromAppleHeartRateZones`); that does not change max heart rate. A finished Apple workout's own zone group (`bucketsFromAppleWorkoutZones`) is stored as `device` zones instead of recomputing, so the chart matches Apple, including time below the first boundary.\n- Prefer the shared timezone helpers from `@workspace/shared` and `SparkyFitnessServer/utils/timezoneLoader.ts` for day-string logic. Avoid `toISOString().split('T')[0]` for user-facing or business-logic dates.\n- Keep `YYYY-MM-DD` values as calendar-day strings until you reach a database or external API boundary that needs UTC instants.\n- Auth or API contract changes usually need a quick check in both web and mobile because they share the same backend.\n- Frontend local dev proxies `/api`, `/health-data`, and `/uploads` to the server on `3010`. The `/health-data` proxy is rewritten to `/api/health-data`, while server APIs remain rooted at `/api`.\n- Server runtime secrets are usually sourced from repo-root `.env`, commonly created from `docker/.env.example`. The server can also load secret files via `SparkyFitnessServer/utils/secretLoader.ts`.\n- Extract shared logic on the **second** duplication (\"rule of two\"), not the third - duplicated logic drifts as different sessions edit each copy. Extract *behavior*, not coincidental shape. See `agent-docs/anti-patterns.md`.\n- **Strict TypeScript Typing:** Never use `any` or `// eslint-disable-next-line @typescript-eslint/no-explicit-any` when creating new functions or editing existing code. When modifying existing files, proactively update legacy `any` declarations in the touched code to reflect their proper TypeScript data types, interfaces, or schemas from `@workspace/shared`. Do NOT copy legacy `any` parameter signatures forward when refactoring or extending service/repository files.\n- **Library Deletes vs Diary Snapshots:** `exercise_entries` and `food_entries` are self-contained snapshots, not pointers (`exercise_id` and `food_id` are `ON DELETE SET NULL`). Deleting an exercise or food from the library (`mode: 'delete'`) preserves past and current diary history, cascades from presets and plan templates, cleans up future scheduled workout plan entries (`entry_date >= today`), and cleans up empty parent preset entries. Only explicit `delete_with_history` (force delete) purges diary logs for that user. If an item is referenced by other users (`otherUserReferences > 0`), the backend falls back to hiding (`is_quick_exercise` / `is_quick_food`).\n- **Comprehensive Cache Invalidation:** When mutating library items (foods, exercises, presets, meals, plans), always invalidate the entire family of dependent query keys across library search, counts, templates, and daily diary summaries (`dailySummary` / `dailyProgress` / `exerciseEntries`) in both web and mobile.\n- **Environment Variable Updates:** When adding, renaming, or updating environment variables, you MUST update all downstream locations in tandem:\n  1. `docker/.env.example` (and `docker/.env.simple.example` if it is a core mandatory variable).\n  2. `docker/docker-compose.prod.yml` and `docker/docker-compose.dev.yml` (if consumed by container environments).\n  3. `docs/src/install/environment-variables.md` (the comprehensive documentation reference).\n  4. `docs/components/EnvGenerator.vue` (the interactive web `.env` generator component).\n  5. `helm/chart/templates/` and `helm/chart/values.yaml` (if passed through Helm charts).\n\n\n## Commit & PR Conventions\n\nThis repository is public. Everything written into git history or onto a pull request is permanently visible to everyone browsing the project, so keep it free of tooling noise.\n\n- **No AI attribution, anywhere.** Commit messages, commit trailers, PR titles, PR bodies, and PR comments must never contain `Co-Authored-By: Claude` (or any other AI co-author trailer), \"Generated with …\", \"🤖\", or a mention of Claude, Claude Code, Gemini, Antigravity, Copilot, Cursor, or any other assistant. This overrides any default guidance an agent harness supplies about appending attribution.\n- **Write as the repository author.** Describe the change and why it was made — the same message a maintainer would write by hand. Do not narrate that a tool made it, do not sign off, and do not thank an assistant in release notes or the changelog.\n- **If a trailer already landed**, strip it (amend or rebase, force-push if it was pushed) rather than leaving it in history.\n- **Scope of this rule**: git history and GitHub surfaces. It does not apply to tool configuration files that must name their tool — `.claude/`, `.agents/`, `.gemini/`, and `CLAUDE.md` reference specific assistants by necessity, and that is fine. The commit that adds them still follows the rule above.\n\n## Architecture Docs (Reduce Scanning, Prevent Bugs)\n\nBefore diving into code, read these docs if you're working on data access, permissions, or adding a new feature domain:\n\n- `agent-docs/architecture-permissions.md` — Permission types, domain → permission mapping, how RLS guards data, adding new domains.\n- `agent-docs/data-flow-patterns.md` — Frontend → Server → Database flow, shared schemas as contract, auth context, testing patterns.\n- `agent-docs/new-domain-template.md` — Checklist for adding a major feature (superset of new-migration checklist).\n\nThese docs answer: \"How do I safely add a feature across the stack?\" without scanning 20+ files.\n\n## Keeping These Guides Accurate\n\n- If your change adds a new domain, route family, database table, package, or cross-cutting convention, update the affected `AGENTS.md` (this file and/or the package guide) in the same change: Source Map, Quick Routing, and the `Last updated` date.\n- Stale guides are worse than no guides; when you notice a claim in any `AGENTS.md` that contradicts the code, fix the guide as part of your change.\n\n## Common Commands\n\nUse the package guide for fuller validation and platform-specific workflows. These are the common entrypoints:\n\n### Frontend (`SparkyFitnessFrontend/`)\n\n```bash\npnpm dev\npnpm run validate\npnpm test\n```\n\n### Server (`SparkyFitnessServer/`)\n\n```bash\npnpm start\npnpm run validate\npnpm test\npnpm run test:coverage\n```\n\n### Mobile (`SparkyFitnessMobile/`)\n\n```bash\npnpm start\npnpm run ios\npnpm run android\npnpm run validate\npnpm exec jest --watchman=false --runInBand\nnpx expo prebuild --clean\n```\n\n### Docs (`docs/`)\n\n```bash\npnpm dev\npnpm run build\n```\n\n<!-- File: shared/AGENTS.md (Tokens: ~2189 | Category: root-instruction) -->\n# AGENTS.md\n\n_Last updated: 2026-10-22_\n\n`@workspace/shared` is a source-first TypeScript workspace library package for schemas, constants, and timezone/day helpers consumed by SparkyFitnessServer, SparkyFitnessFrontend, and SparkyFitnessMobile.\n\n## Scope\n\n- This package defines contracts and shared logic, not an app.\n- Validate changes from consuming packages (server, frontend, mobile), not in isolation.\n- Every schema change here potentially touches three packages.\n\n## Food Assistant State\n\n`schemas/api/FoodAssistant.api.zod.ts` and `schemas/database/FoodAssistant.zod.ts` define lasting preferences, versioned tasks, unresolved ingredients and idempotent operation records. All state is owner-only; these additive contracts do not change existing chat clients.\n\n## Structure\n\n- `src/schemas/database/` - one Zod file per table (`Foods.zod.ts`, `Exercises.zod.ts`, ~115 files). Agent shortcut: to learn a table shape, read the matching file here instead of the SQL dump.\n- `src/schemas/api/` - API request/response contracts (`*api.zod.ts`). `VoiceTranscription.api.zod.ts` defines multipart limits/MIME types, transcription responses, voice preference patches, and compatible public service options shared by web/mobile/server.\n- `src/constants/` - shared constants and enums (exercises, nutrients, meal types, fasting protocols, medication schedules, cycle phases, etc.).\n- `src/utils/` - timezone helpers (`todayInZone`, `instantToDay`, `instantForDay`, `dayToUtcRange`, `compareDays`, `addDays`, `isDayString`), cycle/menstruation helpers, unit/calculation utilities, the race plan generator (`racePlan.ts`, `buildRacePlan`), the run program schedule helpers (`runProgramSchedule.ts`, `runProgramCatchUp.ts`, `runProgramTimeline.ts`: `buildProgramTimeline` / `itemsByDay`, and `runProgramActions.ts`: the `PUT /api/v2/run-program` body builders; web and mobile both use them), and the training pace zones (`racePaceZones.ts`: `racePaceZones` turns a recent race result into easy, interval, tempo, marathon-pace and stride ranges with Riegel's formula; `equivalent5kPace` is the shared 5K conversion; `paceZones.ts` holds the effort-based `paceZones`, `thresholdPaceFromEffort` and `PACE_ZONE_IDS`), and the plan realigner (`runProgramRealign.ts` (`realignProgram` rebuilds the rest of a plan to end on the goal day: drops repeats and the shortest easy runs first, folds lighter weeks, never touches the taper or final workout, keeps two runs a week for three-plus run days, stretches with repeated base weeks when there is room, eases after a long gap, and reports an unrealistic goal with the options; `applyRealign` is the confirmed change, `toRealignResult` the API shape); its contract is in `RunProgram.api.zod.ts`: `realignRunProgramBodySchema`, `runProgramRealignResultSchema`).\n- `src/utils/heartRateZones.ts` - the single source of truth for heart rate zones: `resolveMaxHr` (own value, else Nes from date of birth, else observed max, else 190), `resolveHeartRateZones` (three methods: `percent_max`, `heart_rate_reserve` (Karvonen, needs resting HR) and `custom` floors; falls back to percent of max when the chosen method lacks input and says so in `effectiveMethod`), `heartRateZoneRange(resolved, zone)` (bpm range of zone 1-5; the top zone ends at max HR - the function for training programs that need \"zone N in bpm\"), `computeTimeInZones` (buckets a heart rate series), `percentMaxFloors` / `heartRateReserveFloors`, `floorsFromAppleHeartRateZones` (five custom floors from Apple's preferred zones; null unless there are exactly five, and a missing zone-1 minimum becomes 30 bpm), `bucketsFromAppleWorkoutZones` (a finished workout's five zone durations, keeping time below the first boundary as zone 1; empty when Apple reported no time, null when the group is not five ascending zones). `trainingLoad.ts` weights sessions by these zones. The API contract is `src/schemas/api/HeartRateZones.api.zod.ts`.\n- `src/nutrients/` - nutrient catalogs and the goal calculator used by web onboarding, the web Goals editor and mobile Goals (`nutrientCalculation.ts`: per-nutrient recommended values from age, sex, weight, calories and the saved algorithm choices; `nutrientAlgorithms.ts`: the algorithm enums and labels).\n- `src/ai/`, `src/cycle/`, `src/medications/`, `src/mood/` - domain-specific helpers. `src/medications/adherence.ts` is the one implementation of medication adherence for web and mobile (`computeAdherence`, `computeChecklistProgress`, `entryMatchesDue`, and the month-calendar helpers `tallyEntriesByDay`, `dayLogStatus`, `tallySymptomsByDay`); `subtype.ts` holds the All | Meds | Supplements filter rules. Both are pure, take timezone and data as arguments, and are tested in `SparkyFitnessServer/tests/medicationAdherence.test.ts` and `medicationSubtype.test.ts`.\n- `src/types/` - shared plain TypeScript types that are not Zod-derived.\n- Routes: `src/schemas/api/Route.api.zod.ts` (points, turn cues, bodies), `src/utils/routeGeometry.ts` (distance, climb, simplify, bounds, and the `localProjector` / `distanceToSegment` nearest-point helpers the route editor uses), `src/utils/routeGpx.ts` (dependency-free GPX read/write), `src/utils/routeGenerate.ts` (generate and follow-roads request builders, error kinds, turn counts) and `src/constants/routingProviderTypes.ts` (the `valhalla` external provider type and its default public server URL).\n- Routes: `src/schemas/api/Route.api.zod.ts` (points, turn cues, bodies), `src/utils/routeGeometry.ts` (distance, climb, simplify, bounds) and `src/utils/routeGpx.ts` (dependency-free GPX read/write).\n- `src/compare/` - compare any two tracked things: `stats.ts` (statistics), `metrics.ts` (metric catalog: kind, missing-day rule, weekly form, and `rolling` for a value read over the 7 days ending on the day), `analyze.ts` (day and week-by-week analysis), `since.ts` (before vs. since a habit started: suggested start, stretches, regularity, pre-trend; its limits are exported as `SINCE_*` constants), `question.ts` (the question model web and mobile share: timing choices, spans and `compareWindow`, custom-range limits, since-start bounds, `SUGGESTED_QUESTIONS`, and the recents helpers `knownCompareQuestions` / `withRecentQuestion`), `chartData.ts` (chart data both apps draw: `pairedDots` and `displayDots`, `trendLine`, `overTimeData`, `sinceChartData`). The API contract is `src/schemas/api/Compare.api.zod.ts`.\n- `src/symptoms/` - generic symptom tracking: constants and enums (scales, templates, sections, option kinds), template section resolution (`resolveSections`), built-in symptoms and pick-lists, head/body region ids for the location maps, and custom-field validation. The API contract is `src/schemas/api/Symptoms.api.zod.ts`.\n\n## Naming Convention\n\n- `X.api.zod.ts` = API request/response schema\n- `X.zod.ts` = database table schema\n- Export everything from `src/index.ts`; consuming packages import both types and values via `@workspace/shared`\n\n## Cross-Package Contract Rules\n\n- Changes to `src/schemas/api/` usually affect server routes and both frontend/mobile API clients.\n- Changes to `src/schemas/database/` require matching server migrations and RLS policies when changing the stored schema. CI regenerates the schema backup after merge; never hand-edit or regenerate it locally. Mirroring existing columns missing from a Zod schema does not require another migration.\n- Timezone/day-string helpers prevent bugs; prefer them over `toISOString().split('T')[0]`.\n- Verified commands (run from `shared/`): `pnpm run typecheck`, `pnpm run format:check`, `pnpm run format`, `pnpm run i18n:audit`, `pnpm run validate` (typecheck + i18n audit + format check). The package has no lint or tests of its own; behavior is covered by the consumers' test suites.\n- Shared exports ids and data; apps own translation. `pnpm run i18n:audit` (`scripts/audit-user-facing-strings.mjs`) fails when a file gains more prose in `label`/`title`/`description`/`message`/`hint`/`summary`/`body`/`text` properties than `scripts/user-facing-strings.baseline.json` allows (the cycle, GLP-1, and body-region English catalogs are grandfathered). After moving text out, run it with `--update-baseline` to lower the allowance; use `// i18n-ignore` for literals that must stay.\n- Formatting uses the same `.prettierrc` as the other packages, and lint-staged formats staged `shared/` files on commit.\n- CI (`.github/workflows/ci-tests.yml`) runs `Shared Validate` and re-runs the frontend, mobile, and server jobs on any `shared/**` change.\n- Test any shared change from the consumer packages (`pnpm run validate` in SparkyFitnessServer, SparkyFitnessFrontend, and SparkyFitnessMobile after modifying shared).\n\n## Working Rules\n\n- Keep this package export-focused and schema-focused; logic that scales should live in consuming packages.\n- Never export stale or unfinished types; if a consumer is drafting code and needs a type not yet here, add it.\n\n<!-- File: SparkyFitnessFrontend/AGENTS.md (Tokens: ~4346 | Category: root-instruction) -->\n# AGENTS.md\n\n_Last updated: 2026-10-23_\n\nSparkyFitness Frontend is the React web app for the SparkyFitness monorepo. Use this file as the primary guide for work inside `SparkyFitnessFrontend/`.\n\nIf a task also touches the server, mobile app, or `shared/`, read that package guide before editing outside this directory. Use `../AGENTS.md` for monorepo-level context.\n\n## Scope\n\n- This file is for package-local work in `SparkyFitnessFrontend/`.\n- `CLAUDE.md` just imports this file via `See @AGENTS.md`.\n- Run scripts from this directory.\n\n## Current Snapshot\n\n- Stack: React 19, Vite 8, TypeScript 6, Tailwind CSS v4 (via `@tailwindcss/vite`), shadcn/ui-style Radix primitives, TanStack Query 5, React Router 7 (`createBrowserRouter`), i18next, Better Auth client, Zod 4, Recharts.\n- `@/*` maps to `src/`; `@workspace/shared` maps to `../shared/src/index.ts` (also in Jest via `moduleNameMapper`).\n- Dev server runs on port `8080` and proxies `/api`, `/mcp`, and `/uploads` to the backend on `3010`; `/health-data` is proxied with an `/api` prefix rewrite. Override the backend host with `VITE_BACKEND_HOST`.\n- PWA (`vite-plugin-pwa`) is enabled in production builds only.\n\n## Verified Commands\n\n```bash\npnpm dev\npnpm run typecheck\npnpm run lint\npnpm run format:check\npnpm run format\npnpm run validate\npnpm test\npnpm run test:ci\npnpm run build\n```\n\n- `pnpm run validate` runs typecheck, lint (`--max-warnings 0`), Prettier check, and Knip (`pnpm run knip` for unused files and exports) together.\n- `pnpm test` runs Jest (`ts-jest`, `jsdom`); config is inline in `package.json`, setup in `src/tests/setupTests.ts`.\n- `pnpm run build` runs `validate` first, then `vite build`.\n- CI (`.github/workflows/ci-tests.yml`) runs `pnpm run validate` and `pnpm run test:ci` for this package when its files change; matching those locally means a green PR.\n\n## Domain-Mirrored Layout (the most important convention)\n\nFeatures are organized by domain, and the same domain folder name appears in `src/pages/`, `src/api/`, and `src/hooks/`. A feature change usually touches the matching folder in all three:\n\n- Page domains: `Admin`, `Auth`, `Chat`, `CheckIn`, `Cycle`, `Diary`, `Errors`, `Exercises`, `Fasting`, `Foods`, `Goals`, `Integrations`, `Medications`, `Reports`, `Settings`, `Symptoms`.\n- API domains add a few more: `AiConversions`, `Chatbot`, `Onboarding`, `Pregnancy`, `SleepScience`, `Symptoms`.\n- Example: a Medications bug lives in `src/pages/Medications/` + `src/api/Medications/` + `src/hooks/` medication hooks. Start there, not with a repo-wide search.\n\n## Source Map\n\n- Food assistant state: owner-only preferences, recoverable tasks and operation history use `FoodAssistant.api.zod.ts` / `FoodAssistant.zod.ts` in shared. Server: `routes/v2/foodAssistantRoutes.ts`, `services/foodAssistantService.ts`, `models/foodAssistantRepository.ts`, `ai/tools/foodAssistantTools.ts`. Web: `src/components/ai/FoodAssistantState.tsx`, `src/api/Chatbot/foodAssistantService.ts`.\n\n- `src/main.tsx` - app bootstrap; creates the shared `QueryClient` with global `QueryCache`/`MutationCache` handlers that render toasts from query/mutation `meta` (`errorTitle`, `errorMessage`, `successMessage`).\n- `src/App.tsx` - route registry via `createBrowserRouter`, plus `PrivateRoute` and `PermissionRoute` wrappers (permission-gated areas include `reports` and `admin`).\n- `src/pages/<Domain>/` - route screens by domain.\n- `src/pages/Reports/Compare/` - the Reports > Compare tab (`CompareReport` is the root, mounted by `Reports.tsx` outside its loading gate), with its data from `src/hooks/Reports/useCompare.ts` and `src/api/Reports/compareApi.ts`.\n- `src/api/api.ts` - `apiCall(endpoint, options)` helper: base URL `/api`, query `params`, JSON/FormData bodies, `responseType`, error toasts, `suppress404Toast`. Use it for all backend requests.\n- `src/api/<Domain>/` - per-domain API clients built on `apiCall`.\n- `src/hooks/<Domain>/` and `src/hooks/use*.ts(x)` - TanStack Query hooks and shared UI hooks (`use-toast`, `useDebounce`, `useAuth`, ...).\n- `src/components/` - shared components; `ui/` holds the shadcn-style primitives (~37 files); domain component folders include `Foods/`, `FoodSearch/`, `FoodUnitSelector/`, `Onboarding/`, `ExerciseCharts/`, `ai/` (assistant-ui chat pieces).\n- `src/contexts/` - `ActiveUserContext` (family-access acting-user switching), `PreferencesContext`, `ThemeContext`, `WaterContainerContext`, `ChatbotVisibilityContext`, `ChatToolCategoriesContext` (runtime chat tool-category selection, localStorage-backed).\n- `src/layouts/` - `MainLayout.tsx` and `AddComp.tsx`.\n- `src/lib/` - `auth-client.ts` (Better Auth React client), `utils.ts` (`cn`), scanner engines, sleep helpers.\n- `src/services/` - pure calculation helpers (BMR, body composition, nutrient calculation), not HTTP clients.\n- `src/utils/` - logging, user preferences, date helpers, misc. `browserVoiceInput.ts` owns browser speech/recording lifecycle; `hooks/AI/useVoiceInput.ts` appends transcripts to drafts without sending.\n- `src/tests/` - Jest suites mirroring `components`/`contexts`/`hooks`/`services`/`utils`, plus `test-utils.tsx`.\n- `public/locales/<lng>/translation.json` - i18next resources, loaded over HTTP at runtime.\n\nWhen searching, ignore `node_modules/`, `dist/`, and every locale except `public/locales/en/`.\n\n## Translations (i18n)\n\n- Only ever edit `public/locales/en/translation.json`. The other 35 locales are machine-synced through the `sync-translations.yml` workflow and a separate SparkyFitnessTranslations repo; hand-editing them creates conflicts with that pipeline.\n- UI strings go through `useTranslation()` / `t('...')` keys, not hardcoded literals.\n- `en/translation.json` is ~120 KB - grep for the key or section you need instead of reading the whole file.\n- Developer docs: `../docs/src/developer/translations.md`.\n\n## Conventions\n\n- Use `apiCall` (or an existing per-domain client) for backend requests; don't hand-roll `fetch`.\n- Prefer declaring toast text via React Query `meta` on the query/mutation instead of imperative `toast(...)` calls where the global handlers cover it.\n- Use `src/utils/logging.ts` helpers instead of bare `console.*`; verbosity follows the user's logging-level preference.\n- Keep `YYYY-MM-DD` values as calendar-day strings; use the shared timezone/day helpers from `@workspace/shared` instead of `toISOString().split('T')[0]`.\n- To learn a database table's shape, read `../shared/src/schemas/database/<Table>.zod.ts` - do not read `../db_schema_backup.sql` or the migrations.\n- Auth flows go through `src/lib/auth-client.ts` and `useAuth`; acting-user (family access) state lives in `ActiveUserContext` and affects most data hooks.\n- New UI should reuse `src/components/ui/` primitives and existing shared components before adding new ones.\n- Food and meal quantity fields use `NumericInput` with `allowExpressions` for basic arithmetic. `utils/evaluateArithmetic.ts` parses numbers, parentheses, and `+ - * /` without executing code; invalid expressions block form submission. Other numeric fields retain their existing behavior.\n- Time entry uses `TimeInput` (`'HH:mm'`) and `DateTimeInput` (`'YYYY-MM-DDTHH:mm'`) from `src/components/ui/TimeInput.tsx`, never native `<input type=\"time\">` or `type=\"datetime-local\"`: browsers pick 12h/24h from their language and ignore the user's `timeFormat` preference. Both take the native string formats, so call sites keep their state as-is; name them with `aria-labelledby` (an external `<Label htmlFor>` cannot target a multi-part field).\n- **Cache Invalidation on Library Mutations:** When mutating foods, exercises, presets, or plans, use the domain invalidation hooks from `src/hooks/useInvalidateKeys.ts` (`useExerciseInvalidation`, `useFoodInvalidation`, `useMealInvalidation`, `useDiaryInvalidation`) to invalidate the entire family of dependent query keys including search, presets, templates, and diary daily progress.\n- **Library Deletes & Snapshots:** Deleting foods or exercises uses `mode: 'delete'` which preserves logged diary history (via snapshots) and drops items from presets/plans; only explicit `delete_with_history` deletes diary entries. Empty presets are guarded against starting/logging.\n\n## Testing and Validation\n\n- Test files live in `src/tests/`, mirroring the source area they cover; use `test-utils.tsx` for rendering with providers.\n- Run the tests nearest the touched surface first, then `pnpm run validate` for cross-cutting changes.\n- Lint is strict (`--max-warnings 0`); unused imports fail the build.\n\n## Quick Routing\n\n- Routing/navigation/permission issue: `src/App.tsx` (router, `PrivateRoute`, `PermissionRoute`) and `src/layouts/MainLayout.tsx`.\n- API/error-toast issue: `src/api/api.ts`, then the domain client in `src/api/<Domain>/`, then the query/mutation `meta` in the calling hook.\n- Auth/session issue: `src/lib/auth-client.ts`, `src/hooks/useAuth.tsx`, `src/pages/Auth/`, and the server's `auth.ts` if it crosses packages.\n- Family-access/acting-user issue: `src/contexts/ActiveUserContext.tsx` and the hooks consuming it.\n- Chat (Sparky) issue: `src/pages/Chat/`, `src/components/ai/`, `src/api/Chatbot/`. Voice controls live in `VoiceInputControl.tsx`; `VoiceInputSettings.tsx` shares server-backed enable/System/AI choices with mobile. `active_voice_ai_service_id: null` means System, never automatic inheritance from Chat. A compatible Chat model is only a suggestion. Cancelling, leaving the page, hiding the tab, or changing voice preferences releases the microphone and aborts uploads.\n- Theme/preferences issue: `src/contexts/ThemeContext.tsx`, `src/contexts/PreferencesContext.tsx`, `src/api/Settings/preferences.ts`, `src/utils/userPreferences.ts`.\n- Calorie goal settings: `src/pages/Settings/CalculationSettings.tsx`; `components/WeeklyWeightGoalInput.tsx` edits the `weekly` goal mode with direction and kg, g, lbs or stones + pounds per week, defaulting to the profile weight unit. Store signed kilograms in `goal_mode_weekly_change_kg` (negative loss, positive gain, zero maintenance), preserving legacy percentage modes. Use shared `formatWeeklyGoalRate` / `parseWeeklyGoalRate` for display and input; switching units preserves the canonical rate. Pass `weeklyChangeKg` to `computeCalorieTarget`; Diary preview and `CalorieTargetBreakdown` use the same preference.\n- Missing/wrong UI text: the i18n key in `public/locales/en/translation.json` and the `t('...')` call site.\n- Exercise alternatives / workout feedback / adaptive suggestions (#1560): `AddExerciseDialog`'s `replaceFor` prop adds the **Suggested** tab (`pages/Exercises/ExerciseAlternativesPanel.tsx`); the workout player's Replace (lazy-loaded dialog) and load pass (`WorkoutPlaybackPage.tsx`, helpers in `utils/workoutPlayback.ts`) apply the shared `decideAdaptiveAdjustment` rules and keep both the usual and adapted sets on the draft for \"Use my usual\"; feedback is `pages/Diary/WorkoutFeedbackPanel.tsx` (finish dialog + expanded diary workout) via `hooks/Exercises/useWorkoutCoaching.ts`.\n- Medication adherence, streak, checklist progress and month-calendar status (`pages/Medications/TodayMedications.tsx`, `MedicationLogCalendar.tsx`) are computed by `@workspace/shared` (`medications/adherence.ts`), the same code the mobile adherence screen uses; change the rules there, not in the page. The meds/supplements filter rules (`filterMedsBySubtype`, `filterEntriesBySubtype`, `visibleDoseCards`) come from `medications/subtype.ts`.\n- Symptom or episode tracking (#1882): `src/pages/Symptoms/` (`SymptomsHub` is the tracker, shown on Check-in through `SymptomsPanel` and on Medications with `variant=\"medications\"`; `SymptomLogForm` is template-driven, `SymptomsReport` is Reports > Symptoms), `src/api/Symptoms/symptomService.ts`, `src/hooks/useSymptoms.ts`. Which sections a symptom asks about comes from `resolveSections` in `@workspace/shared`; family delegates with only the symptoms permission use the standalone `/symptoms` page.\n- Bodyweight exercises (#56): the `bodyweight_reps` layout in `constants/exercises.ts` (`signedWeight`) drives the +/− weight header and placeholder in the set editor; reports and trend charts count body weight through the shared `effectiveLoadKg` / `setVolumeKg` (`pages/Reports/ReportsTables.tsx`, `utils/exerciseTrendUtils.ts`, which reads `body_weight_kg` from the progress endpoint).\n- `weight_distance` / `weight_duration` (#127) have their own `SET_TABLE_LAYOUT` entries: carries swap the reps cell for a distance cell (stored km, edited in metres/yards via `carryDistanceFromKm` / `carryDistanceToKm` in `@workspace/shared`), loaded holds drop reps and use the duration column.\n- Training consistency (#59): `pages/Reports/TrainingConsistencyCard.tsx` in the Exercise report's left column, fed by `useTrainingConsistency` (`hooks/Reports/useReports.ts`) from `GET /reports/training-consistency`. A fixed 26-week window computed on the server by the shared `buildTrainingConsistency`, so it ignores the report's date filter; the existing `WorkoutHeatmap` stays the calendar.\n- Compare tab (day and week questions): `src/pages/Reports/Compare/CompareReport.tsx` holds the question and span state and wires the sentence, charts and tables; wording is `compareCopy.ts` and table rows are `compareTables.ts`; question, window and display-scale logic is `shared/src/compare/`. Since-habit questions are not offered on the web yet. It shows only the signed-in user's own data (no request while acting on behalf), and its range is its own, so the Reports date picker is hidden on this tab.\n- Shoe inventory: the Equipment section of the Exercises page (`src/pages/Exercises/EquipmentManager.tsx` + `ShoeFormDialog`; there is no separate tab or route), `src/components/Shoes/ShoeSelect.tsx` (the picker used by `pages/Diary/EditExerciseEntryDialog.tsx` for cardio entries), `src/api/Shoes/shoeService.ts`, `src/hooks/Shoes/useShoes.ts`. Distances are stored in km and edited in the user's unit; assigning a shoe invalidates both `shoeKeys` and `exerciseEntryKeys` because mileage is derived from linked entries. The Equipment section also shows while acting on behalf of someone: `can_manage_diary` delegates can edit it and `can_view_reports` delegates read it, as the server enforces.\n- Training programs (run and walk plans): the `/exercises/training-programs` route (`src/pages/Exercises/TrainingPrograms/`: `TrainingPrograms` is the page, `TrainingProgramPicker` the built-in list with the 3 or 4 runs a week choice and the person's own plans, `StartProgramDialog` the start day, `TrainingProgramCard` the running program, `TrainingProgramCalendar` the month view, projected finish day, goal-date fit and catch-up), reached from `TrainingProgramsEntry` on the Exercises page, and `pages/Diary/RunProgramBanner.tsx` in the Diary's exercise card. Data is `GET/PUT /api/v2/run-program` plus `POST .../complete` and `GET .../custom` through `src/api/Exercises/runPrograms.ts` and `src/hooks/Exercises/useRunProgram.ts`; the server route is owner-only (`requireSelfActor`), so every hook and the banner stay off while `isActingOnBehalf`. All schedule maths comes from `@workspace/shared` (`projectRuns`, `projectedFinishDay`, `programRunForDate`, `catchUpFor`, `buildProgramTimeline`), never re-derived here. Request bodies come from `utils/runPrograms.ts`; `useSaveRunProgramMutation` runs changes one at a time (mutation `scope`) and accepts a function of the program as it stands, so two quick weekday taps both land. Mutations write the response into `runProgramKeys.current()` and call `useRunProgramInvalidation` (program, workout plan, exercise entry and daily progress families); `useDiaryInvalidation` and `useExerciseInvalidation` also refresh `runProgramKeys.all`. English program names, descriptions and week layouts live in `constants/runProgramCopy.ts` and mirror `trainingPrograms.programs.<id>.*` in the locale file. The web does not record runs: **Mark workout as done** (card and Diary) is how a workout is ticked off. Not on web yet: creating or editing custom plans, the race plan builder, the effort rating, the ease-back offers and the workout detail screen.\n- Heart rate zones: Settings → Profile & Account → Heart Rate Zones (`src/pages/Settings/HeartRateZoneSettings.tsx`, `src/hooks/Settings/useHeartRateZones.ts`, `src/api/Settings/heartRateZones.ts` over `GET`/`PUT /api/v2/heart-rate-zones`). Zone maths is shared (`@workspace/shared` `heartRateZones.ts`); labels are in `src/utils/heartRateZoneLabels.ts`. `ActivityHeartRateZonesChart` takes `zoneMethod` (from the stored `zone_method` of the workout's zone rows) and says which method the zones were worked out with; stored zones are never recomputed when settings change.\n- Routing provider (Valhalla, route generation and Follow roads): a Settings > External Providers type like Mealie, with `pages/Settings/RoutingProviderFields.tsx` (empty server URL with an example placeholder and no default server, optional API key, privacy and public-server notes) used by `ProviderSpecificFields` (add) and `EditProviderForm`; the type row is seeded by the server migration, the server resolves it per person (see the server guide).\n- Chart issue: Recharts usage in the domain page plus `src/components/ExerciseCharts/` or `ZoomableChart.tsx`.\n\n## Priority Rule\n\n- For work inside `SparkyFitnessFrontend/`, this file wins over repo-root guidance on package-specific details.\n- If a task spans packages, combine this guide with the other affected package guides.\n- If you add a new domain folder, route family, or cross-cutting convention, update the Domain list, Source Map, and Quick Routing sections of this file in the same change.\n\n<!-- File: SparkyFitnessMobile/AGENTS.md (Tokens: ~11363 | Category: root-instruction) -->\n# AGENTS.md\n\n_Last updated: 2026-10-23_\n\nSparkyFitness Mobile is a React Native 0.86 + Expo SDK 57 app for syncing Apple Health / Health Connect data with the SparkyFitness backend, tracking nutrition, hydration, fasting, measurements, exercise, saved foods, meal templates, custom exercises, workout presets, iOS / Android widgets, the active workout HUD, and the Sparky AI chat.\n\nThis is the package guide for `SparkyFitnessMobile/`. Work from this directory for mobile implementation and validation. If a task crosses into the backend, frontend, or `shared/`, read that package guide too before editing outside mobile.\n\n## Scope And Style\n\n- TypeScript is strict. Keep changes type-safe and compiling cleanly.\n- Prefer small, direct changes that fit the existing screen, hook, and service boundaries.\n- For ambiguous bugs, prove which layer is failing before patching. One narrow diagnostic check beats speculative edits across multiple layers.\n- Do not replace a working implementation with a rewrite unless the requester explicitly approves that direction.\n- When asked to plan work, confirm scope with clarifying questions before exploring code or drafting the plan.\n- Run scripts from `SparkyFitnessMobile/`, except root package operations such as `pnpm install` for patched dependencies.\n- Treat `android/` and `ios/` as generated output when possible. Edit `app.config.ts`, `plugins/`, `targets/`, JS/TS sources, or patch files first, then regenerate with prebuild when needed.\n\n## Stack And Imports\n\n- Primary stack: React 19.2, React Native 0.86, Expo SDK 57, TypeScript 6, React Navigation 7, TanStack Query 5, Uniwind / TailwindCSS v4, Reanimated 4, Skia, Victory Native, Expo Background Task / Task Manager / Notifications, Zustand, assistant-ui + AI SDK (chat).\n- `@/*` maps to this package and `@workspace/shared` maps to `../shared/src/index.ts`.\n- Prefer `@workspace/shared` schemas, constants, date/timezone helpers, and types over local duplicates.\n- The app talks to the backend under `/api`; health uploads go to `POST /api/health-data`.\n- Global `fetch` is Expo's WinterCG `expo/fetch`, so React Native's `{uri, name, type}` FormData file parts throw \"Unsupported FormDataPart implementation\". Append an `expo-file-system` `File` (it implements Blob) for multipart uploads; see `pregnancyPhotosApi.ts`.\n- Server-stored distance/weight units are metric. UI conversion belongs in mobile helpers such as `unitConversions.ts`.\n\n## Localization contract\n\nEnglish (`en`) is the canonical source locale and the deterministic fallback. Feature developers must add/update the English catalog, use semantic static keys, provide explicit English fallback/defaultValue text, use count-based i18next pluralization, use app-locale date/number/unit formatters, and avoid user-facing hardcoded text. Custom, user, and server content stays literal.\n\nFeature developers do not need to know or translate Polish or any future language, and do not need to wait for Weblate. Translators/Weblate own Polish and future translations and linguistic QA. Missing translation content is non-blocking and falls back to English; existing translated content remains structurally validated.\n\nThe shipped locale registry is `src/localization/localeRegistry.json`, read through the typed accessors in `src/localization/localeRegistry.ts`. Adding a catalog to Weblate does not ship it. Shipping requires explicit registry enablement plus native/platform support verification. RN catalogs and native resources are separate surfaces (Expo metadata, Android widget resources, and iOS widget/Live Activity `.lproj` resources), each its own Weblate component and each synced by `.github/workflows/sync-translations.yml`. Only the `en` side of any surface is edited by hand; the i18n audit blocks on registered locales and reports unregistered ones without failing. The workflow pulls the widget resources for registered locales only, because Android and iOS compile whatever resource directories are present and there is no build-time registry check for them.\n\n## Commands\n\n```bash\npnpm start\npnpm run ios\npnpm run android\npnpm run lint\npnpm run typecheck\npnpm run validate\npnpm exec jest --watchman=false --runInBand\npnpm exec jest --watchman=false --runInBand <test-path>\npnpm run test:coverage -- --watchman=false --runInBand\nnpx expo prebuild --clean\n```\n\n- `pnpm run validate` runs i18n and muscle-figure generation checks, typecheck, lint, i18n audit, watch i18n audit, Knip (`pnpm run knip` for unused files and exports), native locales check, and Prettier check together.\n- Use Watchman-disabled Jest commands in agent/sandbox runs; bare Jest often fails on macOS.\n- `collectCoverage` is enabled in Jest config, so expect coverage output from normal test runs.\n- Run `npx expo prebuild --clean` after native dependency changes, permissions, app group or widget target changes, Expo plugin changes, native config edits, or patching native modules.\n- After editing the root `patches/react-native-health-connect@4.1.3.patch`, run `pnpm install` from the repo root, then prebuild from mobile.\n\n## App Shell And Navigation\n\n- `App.tsx` is the root composition point. `App()` wraps `QueryClientProvider`, `KeyboardProvider`, `GestureHandlerRootView`, and `BottomSheetModalProvider`; `AppContent()` owns `NavigationContainer`, `SafeAreaProvider`, navigators, `AddSheet`, auth modals, the embedded/floating active-workout bars, the tab-bar `WhatsNewBanner`, and toasts.\n- App-shell logic lives in dedicated hooks that `AppContent` composes: `useAppBootstrap` (`src/hooks/useAppBootstrap.ts`) owns language initialization, initial-route selection, the initial linking-enabled state, and splash hiding; `useAppStartup` (`src/hooks/useAppStartup.ts`) owns the one-time startup services (theme, notifications, notification actions, background sync, HealthKit observers); `useAutoSyncOnOpen` (`src/hooks/useAutoSyncOnOpen.ts`) for cold-start/foreground-return sync and the observer-yield window; `useAddSheetActions` (`src/hooks/useAddSheetActions.ts`) for AddSheet handlers and last-active-tab tracking. Error-boundary-wrapped `Safe*` screen components live in `src/navigation/safeScreens.tsx`.\n- Startup initializes theme, haptics, sounds, notification prefs, logs, timezone bootstrap, background sync, pending cache refreshes, fasting/hydration card visibility, and platform health observers.\n- Initial route comes from `getActiveServerConfig()` inside `useAppBootstrap`: no active config lands on `Onboarding`; otherwise users enter `Tabs`. A language-initialization failure is logged and never changes the route.\n- Deep links are enabled only after startup confirms `Tabs`, so widget links do not bypass first-run onboarding.\n- `CompareReport` is a stack route (`SafeCompareReport`, title `screens.compareReport`) opened from the Reports hub.\n- Navigation source of truth is `App.tsx` plus `src/types/navigation.ts`; update both and the linking config when routes change.\n- Root stack uses `@react-navigation/native-stack`. Tabs use `@react-navigation/bottom-tabs`.\n- Tabs are `Dashboard`, `Diary`, `Add`, `Library`, and `Settings`. `Add` is a center action in `CustomTabBar`, not a content screen.\n- Native iOS Liquid Glass tabs use `@bottom-tabs/react-navigation` in `src/components/TabsLayout.tsx`; each content tab is wrapped in its own `createNativeStackNavigator` so the tab path still gets native headers. The `Add` tab uses `role: 'search'`, which the iOS 26 SDK draws as the detached round button. The iOS 27 SDK no longer does, so `patches/react-native-bottom-tabs@1.4.0.patch` (a pnpm patch) leaves the role off on iOS 27+ and sets the tab controller's `prominentTabIdentifier` (the iOS 27 API for a separated trailing tab) to that tab instead. Drop the patch once the library does this itself.\n- `TabsLayout` switches at runtime via `useNativeIOSTabsActive()` (`services/nativeTabBarPreference.ts`): native tabs require iOS 26+ Liquid Glass support AND the user's `liquidGlassTabBarEnabled` preference (opt-in). Everything else renders `CustomTabBar` (`TAB_BAR_HEIGHT = 56`); `ActiveWorkoutBar` reads the native tab-bar height instead when native tabs are active.\n- When adding a root-stack screen, add the route to `RootStackParamList` and register a matching `<Stack.Screen>` in `App.tsx` with `createStackScreenOptions(...)` or equivalent explicit iOS native-stack header options.\n- Native header option/button builders live in `utils/nativeHeaderItems.ts` (`createIOSNativeHeaderOptions`, `createIOSSmallNativeHeaderOptions`, text/icon button items); header action colors come from `useHeaderActionColors`, which is Liquid-Glass-aware on iOS.\n- Root-stack screens with a screen-owned React header are automatically checked by `__tests__/navigation/nativeHeaderContract.test.ts`; do not add screen-specific native-header allowlists.\n- Root-stack screens with a screen-owned React header and a real back button must set `headerBackTitle` or `headerBackButtonDisplayMode: 'minimal'` in `App.tsx` so iOS back labels stay explicit or intentionally hidden; close/cancel modal headers do not need either option.\n- Declare screen headers with `useScreenHeader(config)` (`src/hooks/useScreenHeader.tsx`). One declarative descriptor (`title`/`nativeTitle`, `left`/`right` items of kind `back`/`dismiss`/`text`/`icon`/`primary`/`menu`, `busy`/`disabled`, `animateKey` for view↔edit cross-fades) renders both paths: on the native path it mirrors items into `unstable_header{Left,Right}Items` via a layout effect and returns `null`; on the custom path it returns the bar element for the screen to render. A `menu` item is a declarative dropdown (plain actions and/or titled single-select sections, optional accent-dot `showsBadge`): the native path builds a system UIMenu header button, the custom path renders the trigger plus an `AnchoredMenu` under it — one item list drives both, as in `FoodsLibraryScreen`'s ownership filter. Hook screens must not keep hand-rolled header code (no `unstable_header*Items` blocks or custom bars alongside the hook) — the contract test enforces this.\n- Path selection is `useNativeIOSHeadersActive()` (`services/nativeTabBarPreference.ts`): always false on Android; on iOS it is true below iOS 26 (classic native headers) and follows the Liquid Glass toggle on iOS 26+, so turning the toggle off swaps in the same screen-owned fallback headers Android renders.\n- One-accent rule: exactly one primary header action per screen (`kind: 'primary'` or `role: 'primary'`), enforced with a `__DEV__` throw; primary header save buttons fall back to the localized `t('common.save')` / `t('common.saving')` labels when no `label`/`busyLabel` is supplied. Footer-save forms mark their header Save `placement: 'native-only'` so the custom bar does not duplicate the sticky-footer button. `onPress` handlers dispatch through the hook's internal ref map — do not add per-screen handler-ref effects for native header buttons.\n- A **right-slot** `kind: 'primary'` press is wrapped in the shared synchronous duplicate-press guard (`utils/duplicatePress.ts`, same one `FooterSaveBar` uses): presses inside `DUPLICATE_PRESS_WINDOW_MS` collapse to one. `disabled`/`busy` are React state and have not committed when taps queued behind a blocked JS thread replay, so every queued press otherwise ran the handler again (#2191). The guard is deliberately time-based, not a latch on `busy`, because handlers that never report a pending state would leave the button dead. **Left-slot** primaries are exempt — that slot is navigation, and `CycleOnboardingScreen` uses the sugar for a wizard Back where repeated presses are intended. A screen that needs a rapidly repeatable right-slot action must not use `kind: 'primary'`.\n- If a root-stack screen is intentionally presented above `Tabs` instead of inside native-tabs mode, document it in `NATIVE_TABS_ROUTE_EXCLUSIONS` in `__tests__/navigation/nativeHeaderContract.test.ts` with a short reason.\n- Screens intentionally off the hook (e.g. `FoodSearchScreen`'s bespoke anchored-menu bar) must mirror custom actions with `unstable_header{Left,Right}Items` themselves, hide the screen-owned React header behind `useNativeIOSHeadersActive()` with a guard such as `{!usesNativeHeader && <Header />}`, and gate the `useLayoutEffect` that sets native header items on the same flag; otherwise iOS renders both headers.\n- When adding a tab, update `TabParamList`, `NativeTab.Screen`, and `FallbackTab.Screen`; for content tabs also add a tab-local native stack screen using `createIOSNativeHeaderOptions(...)`.\n- `__tests__/navigation/nativeHeaderContract.test.ts` enforces this native-header wiring. If it fails, fix the route/type/navigator alignment instead of weakening the test.\n- Current stack screens include onboarding/tabs, library/detail/form flows for foods/meals/meal plans/exercises/presets, food entry view/edit, meal type detail and copy, the family diary flows (`FamilyMembers`, `FamilyDiary`, `FamilyMealDetail`, and `FamilyCopyReview`), `EditBarcode`, food search/entry/scan/photo flow, workout/activity add/detail, exercise/preset search, settings subscreens, logs, sync, measurements, the progress photo flow (`ProgressPhotos`, `ProgressPhotoCompare`, and `ProgressPhotoTimelapse`), `ExerciseStatistics` and `CardioSession` (with `ActivityShare`, opened from its Share as image button: a 4:5 `ActivityShareCard` of stats and the GPS route over a picked photo or a transparent background, captured with Skia `makeImageFromView` in `services/activityShareImage.ts` and shared as a PNG; color from `utils/shareColor.ts`), `Shoes` and `ShoeForm`, fasting, mindfulness (`MindfulnessDetail` and `MindfulnessLog`), and `WhatsNew`.\n- `AddSheet` offers Food, Workout, Activity, Preset, Measurements, Scan Food, Progress Photos, Mood, Mindfulness, Ask Sparky, and Sync Health Data. Its Mood row opens `MoodLog`, and the Dashboard has a toggleable `mood` card (today's mood, 7-day trend) that opens it (mood faces, tags, note, and a mood-over-time chart; one entry per day via `POST /api/mood`). Its Progress Photos row opens `ProgressPhotos` on the diary's active date. Its Mindfulness row opens `MindfulnessLog` (`MindfulnessLogScreen`), a root-stack modal laid out like `MoodLog` and `MeasurementsAdd` (screen header, scrolling form, sticky `FooterSaveBar`, so the navigator owns Android's Back); `MindfulnessDetail` opens the same screen from the Log Mindful Session row on its Practice & Log tab and, with a `session` param, from Edit on its History tab, so it has no log form of its own. Session type names and icons come from `utils/mindfulnessSessionTypes.ts`, shared by the log screen, the detail screen and `MindfulnessCard`; the list is the shared `mindfulnessSessionTypeSchema`, and a stored type it does not have shows as Other and is kept on save. Paced Breathing and the Meditation Timer keep Finish & Save on screen while the save runs, so they go through `useSaveOncePerRun`: a run is saved once (presses are dropped until the practice is reset, taps replayed before that reset renders included), a failed save can be retried, and a save from a run closed while pending does not reset the next one. `MindfulnessLog` goes back after a save only while it is still focused, because it can be opened from a screen other than the tab host. Keep `AddSheet`'s present/dismiss refs intact to avoid Android re-present loops. Android's hardware Back in `AddSheet` does what the sheet's own Back link does: from the Exercise menu it returns to the main menu, from the main menu it closes the sheet.\n- Current stack screens include onboarding/tabs, library/detail/form flows for foods/meals/meal plans/exercises/presets, food entry view/edit, meal type detail and copy, the family diary flows (`FamilyMembers`, `FamilyDiary`, `FamilyMealDetail`, and `FamilyCopyReview`), `EditBarcode`, food search/entry/scan/photo flow, workout/activity add/detail, exercise/preset search, settings subscreens, logs, sync, measurements, the progress photo flow (`ProgressPhotos`, `ProgressPhotoCompare`, and `ProgressPhotoTimelapse`), `ExerciseStatistics` and `CardioSession`, `Shoes` and `ShoeForm`, fasting, mindfulness (`MindfulnessDetail` and `MindfulnessLog`), and `WhatsNew`.\n- `AddSheet` offers Food, Workout, Activity, Preset, Measurements, Scan Food, Progress Photos, Mood, Mindfulness, Ask Sparky, and Sync Health Data. Its Mood row opens `MoodLog`, and the Dashboard has a toggleable `mood` card (today's mood, 7-day trend) that opens it (mood faces, tags, a Journal box, and a mood-over-time chart). `useMoodCheckinsSupport` probes `GET /api/mood/date/<today>` with `X-Mood-Model-Version: 2` the first time the `mood` card, `MoodLog` or `MoodDay` mounts, and the answer is cached for the session: an array back means the server takes several check-ins a day and any other answer keeps the one-entry-per-day screen via `POST /api/mood`. A failed probe with nothing known shows a Retry on `MoodLog` and `MoodDay` (`components/mood/MoodSupportCheckFailed`) rather than either form, because the one-entry POST on a v2 server edits the day's latest check-in instead of adding one. On a v2 server the `mood` card shows one mood per day (the average of your own moods, else the Garmin or imported one), its body always opens `MoodDay` (the day's check-ins in time order with the day's averages; tap to edit in `MoodLog`, swipe or long-press to delete, Add check-in for a new one), which replaces itself with `MoodLog` for the day on an older server, and its \"+ Log\" (shown when no mood is logged) opens `MoodLog` for a new check-in. `MoodLog` takes an optional `checkinId` to edit; a new check-in for today goes on the profile zone's today even when the dashboard passed the device's day (`utils/moodDay.ts` `moodDayInZone`, which `MoodDay` also uses so its list shows that day; its redirect to the one-entry form keeps the requested day); each face writes the shared `moodFaceValue` (Sad 10 to Excited 90, as on the web); every field is optional but at least one is required, the mood can be left unset (tapping the selected face clears it), the time row opens `TimeSheet` and stays on the current time until set, and edit mode has Delete check-in. The mood report and history chart use one value per day. It also has optional 1-5 Energy and Focus taps (`components/mood/RatingSelector`) that are sent only when changed on the screen (absent means keep, `null` means clear); `useMoodRatingsSupport` applies only to the one-entry-per-day form; it hides them when the server's mood rows lack `energy_value` and shows them when there are no rows to tell, and a save whose response lacks the key shows a one-time \"update your server\" toast. Its Progress Photos row opens `ProgressPhotos` on the diary's active date. Its Mindfulness row opens `MindfulnessLog` (`MindfulnessLogScreen`), a root-stack modal laid out like `MoodLog` and `MeasurementsAdd` (screen header, scrolling form, sticky `FooterSaveBar`, so the navigator owns Android's Back); `MindfulnessDetail` opens the same screen from the Log Mindful Session row on its Practice & Log tab and, with a `session` param, from Edit on its History tab, so it has no log form of its own. Session type names and icons come from `utils/mindfulnessSessionTypes.ts`, shared by the log screen, the detail screen and `MindfulnessCard`; the list is the shared `mindfulnessSessionTypeSchema`, and a stored type it does not have shows as Other and is kept on save. Paced Breathing and the Meditation Timer keep Finish & Save on screen while the save runs, so they go through `useSaveOncePerRun`: a run is saved once (presses are dropped until the practice is reset, taps replayed before that reset renders included), a failed save can be retried, and a save from a run closed while pending does not reset the next one. `MindfulnessLog` goes back after a save only while it is still focused, because it can be opened from a screen other than the tab host. Keep `AddSheet`'s present/dismiss refs intact to avoid Android re-present loops. Android's hardware Back in `AddSheet` does what the sheet's own Back link does: from the Exercise menu it returns to the main menu, from the main menu it closes the sheet.\n- `useNavigationActionGuard` locks navigation-triggering actions while a native-stack transition is running (idle-callback unlock on re-focus, 5s safety release) so double-taps cannot queue duplicate screens; Library create actions use it.\n- `ActiveWorkoutBar` is mounted outside normal screen trees, uses the root navigation ref, and hides itself on modal/editor routes such as food search/forms/scan/photo, exercise search, workout/activity add, measurements, mindfulness log, and barcode edit.\n- Most screens are wrapped with `withErrorBoundary(...)`; `SettingsScreen` also uses section-level recovery so settings remain reachable.\n\n## Source Map\n\n- `src/components/` - reusable UI, charts, settings rows, custom tab bar, add sheet, workout HUD, form chrome, library rows, diary rows, serving sheets, food/workout editors, fasting UI, writeback UI, and `ui/` primitives.\n- `src/components/auth/` - MFA UI shared by onboarding, setup, and reauth.\n- `src/components/compare/` - the Compare report's sentence, span control, start state, answer/checks cards and dots/bars/over-time charts, plus the since-a-habit-started pieces (`CompareSinceRows`, `CompareSinceCard`, `CompareSinceChart`); copy comes from `utils/compareCopy.ts` / `utils/compareSinceCopy.ts`, the since line steps from `utils/compareSinceLine.ts`, metric labels from `constants/compareMetrics.ts`; the question model (spans, windows, suggested questions, recents helpers) and chart data (paired dots, trend line, over-time and since chart data) come from `@workspace/shared` (`shared/src/compare/question.ts`, `shared/src/compare/chartData.ts`).\n- `src/components/glp1/` - GLP-1 tracker cards (injection log, site body map, modeled level, pen/vial inventory, titration, daily check-in) used by `Glp1TrackerScreen`; hooks in `useGlp1.ts` (query keys sit under the `medications` root so `invalidateMedicationEntryCaches` refreshes them). Body-map geometry and the contract types come from `@workspace/shared`.\n- `src/components/medications/` - dose rows, the medication list row and the adherence section: `AdherenceOverviewCard` (stat tiles, ring, 14-day bars, streak), `MedicationChecklistCard`, `MedicationDoseListCard` (due and as-needed rows over `DoseRow`), `MedicationLogCalendar` (month grid, medication filter, legend, symptom dots; owns its own entries and symptom queries), `MedicationDayLog` (the selected day's entries with edit and delete) and `useMedicationStatusColors` (theme tokens for all of them). The screens are `MedicationAdherenceScreen` (route `MedicationAdherence`, opened from the \"Adherence & log\" row on `MedicationsListScreen`) and `MedicationEntryEditScreen` (route `MedicationEntryEdit`, a modal that takes the entry as a param). All adherence, streak, checklist and calendar-status maths comes from `@workspace/shared` (`medications/adherence.ts`, shared with the web Medications page); `hooks/useMedicationAdherence.ts` feeds it one entries window ending on the selected day, and `utils/medicationAdherence.ts` holds the mobile-only helpers (as-needed selection, month shifting, edited-time conversion). The `All | Meds | Supplements` filter helpers also live in shared (`medications/subtype.ts`). Every dose change goes through `invalidateMedicationEntryCaches`, which refreshes the diary card, adherence window, month calendar and daily summary together; injection rows are edited and deleted through the injection endpoints (`useUpdateInjection`, `useDeleteInjection`). The calendar's symptom dots read `useSymptomEntries`, need the separate symptoms permission (without it the dots are simply absent), and never show `source: 'cycle'` symptoms.\n- `src/screens/` - top-level route destinations: dashboard, diary, family member/diary/meal/copy-review flows, settings, sync, logs, Whats New, fasting, food search/scan/photo, library CRUD flows, workout/activity flows, and measurement entry.\n- `src/navigation/` - navigation-level modules such as `safeScreens.tsx`, the error-boundary-wrapped screen components registered in `App.tsx`. (`FoodPhotoFlow` lives in `src/components/`.)\n- `src/hooks/` - TanStack Query hooks, auth/connection hooks, library/search/mutation hooks, measurement/water/check-in hooks, fasting hooks, workout form hooks, widget sync, query client, query keys, and cache helpers.\n- `src/services/api/` - backend clients. `apiClient.ts` handles normal API auth/proxy headers; `healthDataApi.ts`, `aiSettingsApi.ts`, food-photo estimate, and other raw fetch paths must keep auth, proxy, timeout, and session-expiry behavior aligned.\n- `src/services/healthconnect/` - Android Health Connect reads, native aggregation, transformation, enrichment, preferences, and writeback.\n- `src/services/healthkit/` - iOS HealthKit reads, statistics aggregation, transformation, background delivery, preferences, and writeback.\n- `src/services/shared/` - platform-agnostic health helpers: the `collectHealthData` / `runForegroundSync` engine both orchestrators share, the per-run workout-telemetry budget and its reuse cache, Health Connect error classification, sample downsampling, day aggregation/transformation, preference factories, and permission migration/sets.\n- `src/services/` - platform health orchestration, writeback re-exports, background sync, auto-sync coordination, diagnostics, calculations, logging, storage, theme, haptics, sounds, notifications, food photo intro, meal selection, boolean preferences, card visibility, and workout drafts.\n- `src/stores/` - Zustand stores, including the persisted active workout/rest timer store.\n- `src/utils/` - date helpers, unit conversion, food details, meal nutrition, nutrient display, workout/session helpers, fasting formatting, numeric input, concurrency, sync utilities, duplicate-press guarding, photo estimate error mapping, and rate limiting.\n- `src/constants/` - meal, exercise, fasting, and nutrient metadata.\n- JS bridges to native modules live in `src/services/` (`CalorieWidgetBridge.ts`, `ExactAlarmBridge.ts`); there is no `src/native/` directory.\n- `plugins/`, `targets/widget/`, `targets/android-widget/`, `targets/android-exact-alarm/` - Expo plugins and widget/native extension sources.\n\n## React Query And Local State\n\n- Query setup lives in `src/hooks/queryClient.ts`; keys live in `src/hooks/queryKeys.ts`.\n- Default `staleTime` is `Infinity`, so mutations must explicitly invalidate or update affected caches.\n- `refreshHealthSyncCache` invalidates daily summary, measurements, sleep (`sleepDayQueryKey` / `sleepRangeQueryKey`), and exercise history after a health sync. Sleep is in that set because a partial observer upload would otherwise sit in the diary until the app is reloaded.\n- `useRefetchOnFocus(refetch, enabled)` is the standard focus-refresh hook.\n- `useCompare(question, window)` wraps `fetchCompare` on `compareQueryKey` for a day or week question; it is disabled until the question is askable (and for a since question), refetches on focus, and reports `serverTooOld` for a 404. `useCompareSince(question, habitStart, line)` wraps `fetchCompareSince` on `compareSinceQueryKey` (`habitStart` and `line` are null until picked and then left to the server); its `serverTooOld` is a 404 or a 400 whose JSON body names `details.mode` (an older server rejecting `mode=since`), while any other 400, such as a start out of range, is an ordinary error. `compareRecentQuestions` (appPreferencesStore, newest first, max 5) is written by `CompareReportScreen` once a distinct question, since questions included, has an answer.\n- `useFoodsLibrary` is an intentional exception with an infinite query, finite stale window, and `resetQueries(...)` refreshes so focus/pull refresh reloads page 1 instead of every cached page.\n- Meal mutations invalidate meals, recent meals, search, and details; food entry creation can affect recent meals.\n- Exercise/workout preset list/search/detail invalidation belongs in their mutation hooks.\n- `useUpsertCheckIn` updates measurement queries and calls `refreshHealthSyncCache(queryClient)`.\n- `useWaterIntakeMutation` fetches `waterContainersQueryKey`, persists the selected container, and optimistically updates `dailySummaryQueryKey(date)`.\n- Active-server switches clear React Query state before refetching connection state.\n- Error-boundary retry flows call `queryClient.resetQueries()`.\n- App-local toggles live in `stores/appPreferencesStore.ts` (Zustand `persist`, single AsyncStorage key `@SparkyFitness/app-preferences`): haptics, sounds, notifications, hydration/fasting card visibility, Ask Sparky and progress photos card visibility, the Liquid Glass tab bar opt-in, the active-workout metric column, the default rest period (`defaultRestSec`, edited in `WorkoutSettingsScreen`), mood check-in reminders (`checkinRemindersEnabled`, `checkinReminderTimes`, edited in `NotificationSettingsScreen` and offered once after the first check-in), hydration reminders (`waterReminderEnabled`, `waterReminderIntervalHours`, `waterReminderWindowStart` / `waterReminderWindowEnd`, edited in `NotificationSettingsScreen`), the Health Trends layout (`healthTrendOrder` + `hiddenHealthTrends`, edited in `HealthTrendsSettingsScreen`), the Reports layout (`reportOrder`, `hiddenReports`, `reportDefaultRange`, `hiddenReportSections`, `reportNutrientOrder`, `shownReportNutrients`, edited in `ReportsSettingsScreen`), the Compare report's recent questions (`compareRecentQuestions`, newest first, max 5, written by `CompareReportScreen` and read through `knownCompareQuestions`, which drops any a later build no longer knows), and the Apple Watch layout (`watchPageOrder` + `hiddenWatchPages`, the Goals page's `watchNutrientOrder` + `shownWatchNutrients`, and `watchSetInputStyle`, all edited in `WatchSettingsScreen`). The Health Trends pair is written through the single `setHealthTrendLayout(order, hiddenKeys)` setter rather than one per field, because one drag changes order and visibility together; a saved order is reconciled against `constants/healthTrends.ts` by `resolveHealthTrendOrder`, so registering a new graph needs no `STORE_VERSION` bump. Consume via selectors (`useAppPreferencesStore((s) => s.hapticsEnabled)`) plus generated setters; non-React code reads current values through helpers like `getDefaultRestSec()`. A legacy-aware storage adapter migrates the old per-key `@HealthConnect:*` values once. These preferences never sync to the server.\n\n## Health Sync\n\n- `src/services/healthConnectService.ts` is Android orchestration; `src/services/healthConnectService.ios.ts` is iOS orchestration. They are substantial platform implementations, not thin wrappers.\n- Both orchestrators batch metric reads with `runTasksInBatches`, a concurrency of 3, and per-metric timeouts. Preserve timeout and partial-error handling.\n- Bootstrap timezone state before sync. `ensureTimezoneBootstrapped(...)` runs at startup and `healthDataApi.ts` enforces it before upload.\n- Preserve `record_timezone` and `record_utc_offset_minutes` when available.\n- Manual sync, sync-on-open, foreground-return sync, background sync, and iOS observer-triggered sync share coordination logic. Preserve claim/in-flight guards and cooldown recording.\n- Health uploads are chunked. Simple measurements use large chunks; sleep sessions use smaller session chunks; exercise/workout records are grouped by source to match server delete-then-insert behavior.\n- Sync result objects include `syncErrors`; callers should surface partial failures and avoid advancing `lastSyncedTime` when any metric read failed.\n- `backgroundSyncService.ts` uses overlap windows for sessions and day-aligned rolling windows for cumulative metrics and nutrition. Do not collapse those into one naive window.\n- On iOS, cumulative metrics should use HealthKit statistics queries, not raw sample summation.\n- On Android, cumulative metrics (`Steps`, `Distance`, `ActiveCaloriesBurned`, `TotalCaloriesBurned`, `FloorsClimbed`) use Health Connect `aggregateGroupByPeriod` once per range. Native source-priority dedup should match Health Connect UI; do not reintroduce JS `Math.max` or source allowlist dedup.\n- Android read helpers return `{ records, error }` via `readHealthRecordsDetailed` and `aggregateCumulativeMetricByDayDetailed`; legacy wrappers unwrap only records.\n- Android exercise sessions are enriched with `aggregateRecord` for active, total, and basal calories plus distance and steps over the session window. Active/total calories start scoped to `dataOrigin`, while basal energy remains unfiltered; `total - basal` is compared as an active-energy candidate. Incomplete or implausible active/total pairs retry without the origin filter so Health Connect can apply source priority. Distance and steps always stay origin-scoped; never infer workout steps from an unfiltered clock-window query.\n- iOS exercise-session steps must come from statistics attached directly to the `HKWorkout`. Do not infer workout steps by querying all step samples in the workout's clock window.\n- iOS HealthKit locked-device failures surface as database-inaccessible warnings. Do not treat these as successful empty reads.\n- `app.config.ts` grants `android.permission.health.READ_HEALTH_DATA_HISTORY` so Android can read data older than 30 days.\n- Health Connect permission migrations belong in `services/shared/healthPermissionMigration.ts`, not UI-only state. iOS does not use that version: `refreshEnabledMetricPermissions` asks HealthKit through `hasUnrequestedHealthPermissions` and requests only when an enabled type was never asked about, so bump the version for Android permission changes only.\n- Core check-in measurements use `measurementsApi.ts` and `MeasurementsAddScreen`; preserve `upsertCheckIn` omitted-vs-null semantics.\n- \"Import Full History\" (`ImportHistoryScreen`, reached from `SyncScreen`) is a one-time resumable backfill: `backfillService.ts` walks 30-day day-aligned windows newest-first from start-of-today down to a probe-derived floor (`readEarliestRecord` on both providers), one upload per window, with a per-server checkpoint in `backfillCheckpoint.ts`. It never advances `lastSyncedTime` and never runs writeback; while it runs, `isBackfillRunning()` (autoSyncCoordinator) makes background sync skip.\n- The backfill's metric set is FROZEN in its checkpoint at first run; resume uses the frozen set verbatim and toggle changes require Start Over. Quota exhaustion, locked device, and app-inactive are expected mid-run stops — the checkpoint keeps them resumable.\n\n## Health Writeback\n\n- Writeback sends Sparky diary nutrition and hydration back to Apple Health on iOS and Health Connect on Android.\n- Platform split: `services/writeback.ios.ts` re-exports `healthkit/writeback.ts`; `services/writeback.ts` re-exports `healthconnect/writeback.ts`.\n- `runWriteback()` runs after inbound sync in its own try/catch. Writeback failures must not block inbound sync results.\n- Writeback is opt-in per metric and gated on write permissions. Android production permissions include `WRITE_NUTRITION` and `WRITE_HYDRATION`; other write permissions are dev-only.\n- Imported health entries are skipped to avoid echo loops. iOS sets the app bundle id as the own-source guard; Android relies on source metadata.\n- Per-day content-signature hashing skips unchanged days. Each run deletes prior tracked UUIDs then saves fresh records; failed deletes are retried next run.\n- `HealthDataWriteback` on `SyncScreen` owns the remove flow. `BottomSheetPicker` offers all-time purge or date range through `DateRangeSheet`; both call `removeWrittenData(range)` and clear tracking.\n- Inbound iOS nutrition sync reads food correlations with a rolling nutrition lookback and upserts by `(source, source_id)` server-side.\n\n## Native Patches\n\n- `react-native-health-connect` is declared as `^4.1.3`; the installed build is patched from the repo root via `patchedDependencies` in `pnpm-workspace.yaml`.\n- Patch file: `../patches/react-native-health-connect@4.1.3.patch`, one hunk. `PermissionUtils.mapPermissionResult` maps `ExerciseRoute` and `BackgroundAccessPermission` back to JS but not `ReadHealthDataHistory`, even though `parsePermissions` accepts it on the request path. Since `requestPermission` also resolves through `mapPermissionResult`, a granted history permission can never be observed, so `ensureHistoryReadPermission` always returns false and Import Full History believes it is capped at 30 days. Reported upstream as matinzd/react-native-health-connect#276; drop the patch if that lands.\n- The older 3.5.3 patch also forced `getTimeRangeFilterLocal` in `getAggregateGroupByPeriodRequest` (DST / local-day grouping) and made `ExerciseRouteResultType` string-valued. Both are gone: 4.1.3 uses the local filter at all 21 sites, and upstream is fixing the route enum in #274 by typing the read path as a string literal union instead — a better fix than ours, so `routeNeedsConsent` keeps accepting both forms and depends on neither declaration.\n- `@bacons/apple-targets@4.0.6` is patched via `../patches/@bacons__apple-targets@4.0.6.patch`, fixing two upstream bugs. First, its xcode pass matched \"its\" extension target by type with a fall-back to any same-type target, which adopted and corrupted the expo-widgets `ExpoWidgetsTarget` on a clean prebuild; the patch scopes the match to an exact product-name hit. Second, the existing-target update path crashed every non-clean prebuild (EvanBacon/expo-apple-targets#201): removing the old build configuration list's referrers cleared `target.props.buildConfigurationList`, which the next line then dereferenced; the patch holds the list in a local and iterates a copy of its configurations so none are skipped mid-removal.\n- `@bacons/apple-targets` is held at 4.0.6 on purpose: it has a 5.x, and moving to it invalidates the patch file. `expo install --fix` does not touch either patched package (neither is an Expo SDK package), so SDK upgrades leave both patches applying cleanly.\n- `expo-health-connect` was removed. It only ever supplied an Expo config plugin, and upstream archived it: everything it did ships inside `react-native-health-connect` as of v4, which now writes both the `ACTION_SHOW_PERMISSIONS_RATIONALE` intent filter (Android 13 and below) and the `ViewPermissionUsageActivity` alias (Android 14+). Installing both causes Android build failures from duplicate classes.\n- After changing a patch or upgrading a patched package, run `pnpm install` from the repo root and then `npx expo prebuild --clean` from mobile before native validation.\n\n## Food, Meals, Units, And Photo Estimates\n\n- Food search spans local foods, online providers, meals, barcode scan, label scan, and AI photo estimates. Keep `FoodSearchScreen`, `FoodScanScreen`, `FoodEntryAddScreen`, `FoodFormScreen`, `FoodPhotoFlow`, and route params aligned.\n- `LibraryScreen` is the hub for saved Foods, Meals, recurring Meal Plans, Exercises, and Workout Presets. Full lists live in `FoodsLibraryScreen`, `MealsLibraryScreen`, `MealPlansScreen`, `ExercisesLibraryScreen`, and `WorkoutPresetsLibraryScreen`.\n- Food detail/edit flow: `FoodDetailScreen`, `FoodFormScreen`, `FoodForm`, `FoodUnitSelectorSheet`, `useFoodVariants`, `useFoodsLibrary`, `useDeleteFood`, `foodsApi`, and `utils/foodDetails.ts`.\n- `FoodForm` supports equivalent serving sizes grouped by nutrient signature, auto-scale nutrition, compatible unit conversion via `convertServingSizeOnUnitChange`, optional AI cross-category unit conversion, custom nutrients, and caller-provided `headerChildren`.\n- `EditBarcodeScreen` lets users add or remove extra barcodes for a saved food. Keep `FoodDetailScreen`, `EditBarcodeScreen`, `foodsApi`, and the `EditBarcode` route params aligned.\n- Meal templates use `MealAddScreen`, `MealDetailScreen`, `FoodSearch` / `FoodEntryAdd` with `pickerMode: 'meal-builder'`, and `services/mealBuilderSelection.ts` for pending ingredient handoff.\n- Recurring meal plans use `MealPlansScreen`, `MealPlanFormScreen`, `useMealPlans`, `mealPlansApi`, and `utils/mealPlanForm.ts`. Plans can schedule any meal already visible in the user's meal library, including family/public meals, but the plan itself remains private to its owner.\n- Workout plans (the schedule the watch's \"Scheduled today\" and the diary banner read) are created and edited from `Library > Workout plans` with `WorkoutPlansScreen`, `WorkoutPlanFormScreen`, `useWorkoutPlans`, `workoutPlansApi.ts` and `utils/workoutPlanForm.ts`, modeled on the meal plan screens. The form supports weekly and sequential plans and assigns saved workouts (presets); a single-exercise assignment made on the web is preserved on save but only editable there. Switching the schedule type clears the assignments, as the server expects new ones with a type change.\n- Logged-meal grouped diary entries use `foodEntryMealsApi`, `FoodEntryViewScreen`, and `EditLoggedMealScreen`. Preserve stored component nutrition snapshots when editing.\n- `MealTypeDetailScreen` owns single-meal-type day views and copy-to-another-day via `useCopyFoodEntries`; be careful with custom meal types and synthetic buckets.\n- External food providers use provider-agnostic v2 endpoints where possible. Provider categories and barcode support come from server config; do not hardcode provider type allowlists unless preserving an explicit fallback.\n- \"All Providers\" aggregated search (`useAllProvidersSearch`) fans one debounced term out across every active provider in parallel — one `useQueries` entry per provider, results projected in the `combine` callback for structural sharing. Providers stream in independently; a slow or failing provider must not block the others. Open Food Facts calls go through the shared rate limiter.\n- Label scan tries Apple's on-device model first: `modules/on-device-nutrition` (Swift, `FoundationModels` with the label photo as an `Attachment` plus its text from a Vision `VNRecognizeTextRequest` run in the module (greedy sampling), iOS 27+ with Apple Intelligence; compiled out under `compiler(<6.4)`) is wrapped by `services/onDeviceLabelScan.ts`, which validates the extraction (calories, macros, a per-serving size, and any optional nutrient the model filled in must appear as numbers in the returned `ocr_text`, ranges, sugars/fiber <= carbs, energy vs macros) and returns null on any failure or when the device-local `onDeviceLabelScanEnabled` preference (Settings → AI, `AiSettingsScreen`, shown only when the model is available; it also shows the active server provider, read-only) is off, so `FoodScanScreen.handleUsePhoto` falls back to the server `scanNutritionLabel`. The food form filled in from a scan carries `labelScanSource` and shows `LabelScanSourceBanner`: it says which AI read the label and offers the other one (`useRetryLabelScan` reads the kept photo from `services/labelScanSession.ts` again and replaces the form; coming back to the phone needs the setting on and the model available). The Swift side has not been compiled against the iOS 27 SDK from CI-less environments; verify on an iOS 27 device after `npx expo prebuild --clean`.\n- Photo mode is hidden in meal-builder mode because photo estimates log to the diary.\n- `FoodPhotoFlow` is a modal native stack and wraps itself in a local `KeyboardProvider`.\n- Photo availability fetches `GET /api/chat/ai-service-settings/active` through `aiSettingsApi.ts`; food photo is attempt-all, so `isFoodPhotoAvailable` gates only on a configured provider, not a specific provider type.\n- Estimation posts to `POST /api/foods/estimate-food-photo` through `estimateFoodPhoto(...)` in `externalFoodSearchApi.ts` and uses typed `FoodPhotoEstimateError` codes from `@workspace/shared`.\n- Food-photo request/response changes cross package boundaries: update shared schema and server route/service with mobile.\n- Keep `auto_scale_online_imports` separate from Open Food Facts-specific scaling preferences in `FoodSettingsScreen`.\n- Nutrient management lives under Food Settings: `CustomNutrientsScreen` / `CustomNutrientFormScreen` (create, edit, delete; renames and deletes invalidate display, goals and daily-summary caches since the nutrient name is the key), and `NutrientDisplaySettingsScreen` (`mobile` platform only, and only the view groups a mobile screen reads: `food_database`, which `FoodNutrientBreakdown` applies to the food, meal and entry nutrient lists, and `report_tabular`, which `DailyNutritionDetailsScreen` applies; `summary` and `diary` keep their Dashboard/Diary screens, and `quick_info`, `goal` and `report_chart` are not offered until a mobile screen reads them). A nutrient's goal direction is edited on the Goals screen.\n\n## Exercise, Workouts, And Fasting\n\n- Exercise and workout preset flows use `ExerciseSearch`, `PresetSearch`, detail/form screens, paginated/search hooks, mutation hooks, and shared workout payload helpers in `utils/workoutSession.ts`.\n- Session responses are discriminated unions from `@workspace/shared`: preset workouts and individual activity sessions have different shapes. Keep detail/edit screens type-safe.\n- Workout/activity drafts are persisted by `workoutDraftService`; `useWorkoutForm`, `useActivityForm`, and `useDraftPersistence` own form state.\n- Exercise selection returns via `CommonActions.setParams` and a nonce pattern through `useSelectedExercise`.\n- Adaptive coaching (#1560): every Replace action passes `replaceFor` (built by `utils/exerciseReplace.ts`) so `ExerciseSearch` opens on a ranked **Suggested** tab (`components/ExerciseAlternativesList.tsx`, `GET /api/v2/exercises/:id/alternatives`); catalog picks import through `importExercise` first. End-of-workout feedback is `components/WorkoutFeedbackCard.tsx` (on `WorkoutCompleteScreen` and `WorkoutDetailScreen`) backed by `useWorkoutSessionFeedback`, which autosaves serially so the last answer wins. `useLiveCoachingSignals` (mounted in `ActiveWorkoutScreen`) captures signals into `activeWorkoutStore.coachingSignals` (`null` = asked, no history); `resolveLiveAssumedSetValues` applies the shared `decideAdaptiveAdjustment` rules unless the entry is in `declinedAdaptive`, and `AdaptiveSuggestionBanner` always explains the change with a \"Use my usual\" undo. The `adaptive_workout_suggestions` switch in `WorkoutSettingsScreen` is an account setting; `duckMusicDuringCues` (Lower music during cues) is device-local and drives `beginCueDucking`/`endCueDucking` in `services/sounds.ts` — with it off, the audio session is never touched.\n- Bodyweight exercises (#56) use the `bodyweight_reps` modality: a set's weight is signed (+ added, − assistance, blank = body weight alone). Weight inputs parse it with `parseSignedDecimalInput` (`utils/numericInput.ts`; the base `parseDecimalInput` still rejects a sign) only when the modality is `bodyweight_reps`, including the save builders, and switch to a keyboard with a minus key. Volume and the e1RM/10RM columns go through `setVolumeKg` / `setLoadKg` in `utils/workoutSession.ts`, which delegate to the shared `effectiveLoadKg` rule, with body weight from `hooks/useBodyWeightKg.ts` (the latest check-in on or before the day, fetched only when an exercise on screen is bodyweight). Live PRs stay on the added weight, an unweighted set ranking as +0.\n- Weighted carries and loaded holds (#127) use the `weight_distance` / `weight_duration` modalities. Carry distance is typed in metres (yards when the app distance unit is miles) and converted with `carryDistanceFromKm` / `carryDistanceToKm` from `@workspace/shared` (`utils/carryDistance.ts`); always go through `setDistanceFromKm` / `setDistanceToKm` / `modalityStoresDuration` / `modalityStoresDistance` in `utils/workoutSession.ts` rather than branching on modality. Carry distance is excluded from the workout's cardio distance total. The activity edit flow (`EditableSetRow` / `buildActivitySetsPayload`, which takes the distance unit) edits weight plus seconds or distance for these types.\n- Watch support for them: a loaded hold is armed as `timed` + `weighted` (weight box beside the countdown/stopwatch), a carry as `carry` with `targetDistanceKm`; the watch shows and edits the distance in the phone's unit (metres, or yards when the distance unit is miles; the context carries `distanceUnit`, read as `effectiveCarryUnit`) while `SetValues.distanceKm` / `CompletedSet.distanceKm` stay km on the wire, and `useWatchWorkoutBridge` writes it back as `distance`. The distance keypad accepts a tenth. A pending distance edit is dropped if `distanceUnit` changes before that edit commits, and an open distance keypad is dismissed, same as a weight-unit change drops a pending weight edit. `resolveWatchSetTargets` is the single place those targets are built.\n- Rest timer state lives in `stores/activeWorkoutStore.ts`; notifications are scheduled through `services/notifications.ts`. The rest-complete ping carries a background \"Complete Set\" action (`rest-complete` category): responses are routed to `completeActiveSetIfReady` by `initWorkoutNotificationActions` (exported from the store, wired in App startup — the response listener cannot live in `notifications.ts` without a store↔service import cycle), and stale delivered pings are swept when the next rest is scheduled. Exactly one cue fires per rest: the in-app chime (`services/sounds.ts`) while the app is active, otherwise the ping's own sound. Anything that stands down in favour of the chime must test `willPlayRestCompleteSound()` (preference **and** `AppState`), never the preference alone — the notification handler also runs for frontmost-but-inactive states (screen locking, app switcher), and `markRestReady` cancels the due ping only while the app is active so the deadline flip cannot race the OS delivery. The ping ships at the `timeSensitive` interruption level so a Focus mode cannot silence it.\n- Set IDs are preserved server-side across workout edits so the active w\n\n<!-- File: SparkyFitnessServer/AGENTS.md (Tokens: ~11941 | Category: root-instruction) -->\n# AGENTS.md\n\n_Last updated: 2026-10-22_\n\nSparkyFitness Server is the backend API package for the SparkyFitness monorepo. Use this file as the primary guide for work inside `SparkyFitnessServer/`.\n\n**Quick Links for AI Tools:** See `../agent-docs/README.md` for:\n\n- `file-and-domain-reference.md` — Where to find server code by feature\n- `testing-patterns.md` — How to test routes, services, repositories, and RLS\n- `architecture-permissions.md` — Permission types and RLS patterns\n- `new-migration-checklist.md` — 8-step database change checklist\n\nIf a task also touches `shared/`, the frontend, or the mobile app, read the relevant package guide before editing outside this directory. Use `../AGENTS.md` for monorepo-level context.\n\n## Scope\n\n- This file is for package-local work in `SparkyFitnessServer/`.\n- Keep changes inside this package unless the task clearly crosses package boundaries.\n- This is the single source of truth for the package; `CLAUDE.md` just imports it via `See @AGENTS.md`.\n- Do not invent alternate boot paths, duplicate route registries, or parallel migration flows when the current startup path already covers the behavior.\n\n## Current Snapshot\n\n- Dev boot path: `pnpm start` -> `nodemon` -> `tsx index.ts`\n- `index.ts` loads `../.env`, applies file-backed secrets, runs preflight checks, calls `initializeDatabase()` for migrations and RLS policies, then imports `SparkyFitnessServer.ts`\n- Main app shell: `SparkyFitnessServer.ts`\n- Stack: Express 5, PostgreSQL via `pg`, Better Auth, Zod, TypeScript 6, Vitest 5, ESLint 10\n- Module system: ESM with `type: \"module\"` and `moduleResolution: \"NodeNext\"`\n- The package is now effectively TypeScript-first; almost all source files are `.ts`\n- Main domains: food and meal tracking, exercise logging, health and sleep data, sleep science, fasting, medications, symptom and episode tracking, mood, menstrual cycle and pregnancy, reporting, AI chat, onboarding, identity, admin tooling, and external provider integrations\n\n## Verified Commands\n\n```bash\npnpm start\npnpm run validate\npnpm run typecheck\npnpm run lint\npnpm run lint:fix\npnpm run format:check\npnpm run format\npnpm test\npnpm run test:watch\npnpm run test:coverage\npnpm run test:ci\npnpm exec vitest run tests/mealRoutes.test.ts\npnpm exec eslint routes/v2/foodRoutes.ts services/foodCoreService.ts\n```\n\n- `pnpm start` uses hot reload through `nodemon`; `nodemon.json` ultimately executes `tsx index.ts`\n- `pnpm run validate` runs typecheck, lint, and Prettier check together\n- `pnpm test` runs `vitest run`\n- The backend default port is `3010` unless `SPARKY_FITNESS_SERVER_PORT` overrides it\n- For targeted test runs, prefer `pnpm exec vitest run tests/<name>.test.ts`\n\n## Source Map\n\n- Food assistant state: owner-only preferences, recoverable tasks and operation history use `FoodAssistant.api.zod.ts` / `FoodAssistant.zod.ts` in shared. Server: `routes/v2/foodAssistantRoutes.ts`, `services/foodAssistantService.ts`, `models/foodAssistantRepository.ts`, `ai/tools/foodAssistantTools.ts`. Web: `src/components/ai/FoodAssistantState.tsx`, `src/api/Chatbot/foodAssistantService.ts`.\n\n- `index.ts` - real dev entrypoint; loads env, secrets, and preflight checks before booting the app\n- `SparkyFitnessServer.ts` - Express app shell, route mounting, Swagger/ReDoc, startup, graceful shutdown\n- `services/backgroundJobScheduler.ts` - starts every scheduled background job at startup, including the demo reset; provider syncs are listed in `services/providerSyncScheduler.ts`; `SPARKY_FITNESS_DISABLE_SCHEDULED_JOBS=true` skips them all\n- `auth.ts` - Better Auth configuration, plugins, session behavior, SSO provider syncing\n- `routes/` - primary HTTP route surface\n- `routes/v2/` - newer typed route surface; pair these changes with `schemas/`\n- `routes/v2/openFoodFactsContributionRoutes.ts` - owner-only single-food preview and explicit photo-backed publication; background contributions are disabled for this release\n- `routes/v2/symptomRoutes.ts` - generic symptom tracking (`symptoms` permission): definitions (`/custom`), the pick-list library (`/options`), entries and episodes (`/entries`, `/entries/ongoing`, `/entries/:id/end`, `/entries/:id/severity`), photos, and symptom-free days. Logic lives in `services/symptomService.ts` over `models/symptomRepository.ts` and `models/symptomOptionRepository.ts`; the request/response contract is `../shared/src/schemas/api/Symptoms.api.zod.ts`\n- `routes/reportRoutes.ts` `GET /reports/training-consistency` - the consistency view (#59): `reportService.getTrainingConsistency` reads a fixed 26 weeks of exercise entries and hands them to the shared `buildTrainingConsistency`, so web and mobile show the same streak and weekly sets (`reports` permission)\n- `routes/reportRoutes.ts` `GET /reports/training-load` - acute (7 days) against chronic (4-week average) workout load (#250): `reportService.getTrainingLoad` reads every logged workout through `reportRepository.getTrainingLoadSessions` and the person's heart rate zones (`heartRateZoneService.resolveZonesForUser`), and hands them to the shared `computeTrainingLoad` (`reports` permission)\n- `routes/v2/reportRoutes.ts` - weekly alcohol rollup and the zero-padded hydration/caffeine/alcohol range used by the Trends charts (`reports` permission)\n- `routes/v2/reportRoutes.ts` `GET /compare` - does A go with B (`/api/v2/reports/compare`, owner-only via `requireSelfActor`): parses `compareQuerySchema`, `services/compareService.ts` (`getCompare`) loads both metrics through `models/compareRepository.ts` (today and the current week never counted) and hands them to the shared analysis in `../shared/src/compare/` (`stats.ts`, `metrics.ts` catalog, `analyze.ts` day/week pairing); week mode uses `utils/firstDayOfWeek.ts` and training load is weighted by the person's zones (`heartRateZoneService.resolveZonesForUser`); `mode=since` (before vs since a habit started) parses `compareSinceQuerySchema` instead and calls `getCompareSince`, which hands the shared analysis in `../shared/src/compare/since.ts` the habit over the last 730 days plus two weeks (the analysis holds a suggested start to the last 730 days, `SINCE_START_MAX_DAYS_AGO`, and offers none for a habit already kept at that limit); contract `../shared/src/schemas/api/Compare.api.zod.ts`. The AI tool `ai/tools/compareTools.ts` (`sparky_compare_metrics`, `reports` category, read-only for MCP keys) asks the same two service functions through the shared query schemas and renders the result as text; its English metric names and wording live in that file\n- `routes/v2/nutritionKineticsRoutes.ts` - active-caffeine estimate and bedtime cutoff (`diary` permission)\n- `routes/v2/workoutCoachingRoutes.ts` - adaptive coaching (#1560): session feedback (`workout_feedback`), the per-user `adaptive_workout_suggestions` setting (owner-only write), and recent-history signals (`diary` permission). `GET /v2/exercises/:id/alternatives` (ranked substitutes) lives in `routes/v2/exerciseRoutes.ts`\n- `routes/v2/routeRoutes.ts` - the person's saved routes (`/api/v2/routes`: `GET` summaries, `POST` a drawn route from `points` (+ optional `cues`), `POST /import-gpx` with the file text, `POST /from-activity/:exerciseEntryId` from a recorded GPS track, `GET/PATCH/DELETE /:id` (PATCH renames, changes the activity or replaces the line: `points` + optional `cues` go through `prepareRoute`, which recomputes distance and climb in the same UPDATE and leaves `source`, `source_exercise_entry_id` and `created_at` alone; cues need `points` and every cue index must be below the point count), `GET /:id/gpx` download; 200 per person); owner-only (`requireSelfActor`, `routes` owner policy). `models/routeRepository.ts` stores them; `utils/routePrep.ts` (`prepareRoute`) drops repeated points, measures distance and climb on the full track, thins to 2000 points and re-points cues. Geometry, GPX read/write and the contract are shared: `../shared/src/utils/routeGeometry.ts`, `routeGpx.ts`, `../shared/src/schemas/api/Route.api.zod.ts`\n- `routes/v2/routeRoutes.ts` generation endpoints - `GET /api/v2/routes/generate/capabilities` and `POST /api/v2/routes/generate` (preview only, never saves; owner-only; 10 requests per user per 10 min via `utils/userRateLimiter.ts`). Orchestration is `services/routeGenerationService.ts` (loop resizing, shared `prepareRoute` thinning); the engine sits behind `RoutingProvider` in `services/routing/`. `routingConfig.ts` `resolveRoutingProvider(userId)` picks the engine per request: the user's active `valhalla` external provider first (own, then admin-public, then family-shared via `externalProviderRepository.getActiveProviderWithKeyByType`), else none. The base URL is re-checked with the food-provider network policy and DNS-guarded for non-admin owners, the key comes from the encrypted `app_key` and is sent as `api_key`, and nothing is cached so a new provider applies at once. There are no environment variables for routing. Every engine request carries a `SparkyFitness/<version>` `User-Agent`. The type is seeded by migration `20261021120200_add_valhalla_routing_provider_type.sql` and registered in `BASE_URL_FETCHING_PROVIDER_TYPES` in `externalProviderService.ts`; `valhallaProvider.ts` talks to the engine, pure `valhallaManeuvers.ts` decodes polylines and maps maneuvers to cues, pure `loopPlanner.ts` plans loops. `GET /generate/capabilities` reports `{ available, maxAlternatives: 1, style (all false), stops: false, source: 'provider' | null }` from `getRouteGenerateCapabilities`; the server returns one route per request and `POST /generate` answers `{ route }`. Saving a generated route posts it to `POST /api/v2/routes` with `source: 'generated'` (the only source a client may choose besides `drawn`; migration `20261021120100_allow_generated_route_source.sql` widens the check). Add an engine by implementing `RoutingProvider` and a case in `routingConfig.ts`; never log the provider error object (it can carry the key).\n- `routes/v2/runProgramRoutes.ts` - the person's run program and place in it (`/api/v2/run-program`: `GET`, `PUT` to start, switch on or off, move, or set the Diary schedule (`run_days`, `start_date`, `end_date`), `POST /complete` which also records the person's local `day` as `last_done_date` and, given `entry_id`, appends `{index, entry_id, at}` to `completion_log` (newest 200 kept; cleared on a program switch) so the app can show a diary run as part of the program, `POST /effort` which saves a 1 to 10 rating for a workout already done (`rateRunProgramWorkout`: under the row lock, replaces an earlier rating of the same index, keeps the last 40 in `effort_log`, refused for a workout not yet done or another program; a program switch clears it); the tool's `get_run_program` lists recent ratings and, through shared `suggestFromEffort`, notes two hard or two easy in a row); `POST /realign` rebuilds the rest of the plan to end on the goal day through shared `realignProgram`/`applyRealign` (`../shared/src/utils/runProgramRealign.ts`): without `confirmed: true` it only returns the preview (and the options when the goal is unrealistic) and saves nothing; confirmed, it recomputes under the `adjustRunProgram` row lock (the callback also receives the whole stored `program`), refuses a stale preview `token` with 409 and an unrealistic goal without `force` with 400; the person's local `today` comes from the body or their timezone. After a realignment the stored workouts no longer line up with the built-in definition, so adjusts measure against `builtInBaseline` (built-in workouts only while the lengths match). Owner-only (`requireSelfActor`, `run_programs` owner policy). The person's own plans live in `custom_run_programs` (`GET/POST /custom`, `PUT/DELETE /custom/:id`, 30 per person, up to 364 workouts of timed steps, program id `custom:<uuid>`): starting one through `PUT` copies it into `run_programs` (with `name`/`activity`), editing one also updates the active copy and keeps the place, deleting keeps the active copy. `models/runProgramRepository.ts` seeds the stored `workouts` from the built-in program in `../shared/src/utils/runPrograms.ts`, completes the due workout in one statement so a repeat cannot skip one, and `adjustRunProgram` applies a change under a row lock with a log entry. The change rules (only upcoming workouts, running eased up to 30% or raised at most 10%) are `../shared/src/utils/runProgramAdjust.ts`, shared by the AI tool `ai/tools/runProgramTools.ts` (`sparky_manage_run_program`; every change needs `confirmed=true`; `set_run_program_schedule` changes the Diary days (names or 0 to 6), first day and goal day through the same upsert; `realign_run_program` (fields `force`, `distance`, `confirmed`) previews the same plan and applies it only with `confirmed=true`, and says nothing was changed for an unrealistic goal; `get_run_program` adds a line when the plan has drifted from the goal day; only `get_run_program` is read-only for MCP keys, and the realign POST is not on the read-only allowlist in `utils/apiKeyScope.ts`) and the contract is `../shared/src/schemas/api/RunProgram.api.zod.ts`\n- `routes/v2/heartRateZoneRoutes.ts` - the person's heart rate zone settings (`GET`/`PUT /api/v2/heart-rate-zones`; max and resting heart rate, `percent_max` / `heart_rate_reserve` / `custom` method, custom floors; `reset: true` restores defaults). Owner-only (`requireSelfActor`, also on reads). Logic in `services/heartRateZoneService.ts` over `models/heartRateZoneRepository.ts` (columns on `user_preferences`); `resolveZonesForUser(userId, observedMaxBpm?)` is what stored time in zone (`healthDataHandlers.persistWorkoutTelemetry`, `exerciseEntryService.attachWatchTelemetryToExerciseEntry`) and training load use. Zone maths is `../shared/src/utils/heartRateZones.ts`; the contract is `../shared/src/schemas/api/HeartRateZones.api.zod.ts`. Stored `exercise_entry_hr_zones` rows carry `zone_method` (`device` for Garmin) and are never recomputed when settings change.\n- `routes/v2/mindfulnessRoutes.ts` - mindfulness & meditation sessions tracking (`checkin` permission): day summaries (`/day-summary`), session logs and telemetry (`/entries`, `/entries/:id`). Logic lives in `models/mindfulnessRepository.ts`; the request/response contract is `../shared/src/schemas/api/Mindfulness.api.zod.ts`\n- `routes/v2/shoeRoutes.ts` - running-shoe inventory (`diary` permission): CRUD on `/api/v2/shoes` and `PUT /shoes/assignments/:exerciseEntryId` to link or unlink a diary entry (`exercise_entries.shoe_id`). Logic lives in `models/shoeRepository.ts`; mileage is `starting_distance_km` plus the sum of linked entries' `distance` (km); the contract is `../shared/src/schemas/api/Shoes.api.zod.ts`\n- `routes/auth/` - auth-specific route fragments mounted through `routes/authRoutes.ts`\n- `services/` - business logic and orchestration\n- `models/` - PostgreSQL repositories and persistence helpers\n- `middleware/` - auth, permissions, uploads, and shared Express middleware\n- `utils/uploadsPath.ts` - the uploads root plus the resolver and containment guard for stored `file_path` values; use it instead of re-deriving `SPARKY_FITNESS_CUSTOM_UPLOADS_DIRECTORY`\n- `utils/oauthState.ts` - server-issued single-use OAuth `state` nonces for provider linking (`issueOAuthState`, `persistOAuthState`, `claimOAuthState`); use it instead of hand-rolling a state value\n- `utils/outboundHttp.ts` - process-wide outbound HTTP defaults, applied from `index.ts`: the axios request timeout and the per-address-family connect attempt timeout (`net.setDefaultAutoSelectFamilyAttemptTimeout`). Both are fixed constants on purpose - do not add env overrides, and read the sizing note there before changing either, because the two values interact\n- `utils/errors.ts` - `ValidationError` plus `describeError(error)`; prefer it over `error.message` when logging any caught value, because an `AggregateError` or a non-Error throw renders as an empty string\n- `middleware/requireSelfMiddleware.ts` - `requireSelfActor`, which rejects a switched/delegated context outright; attach per-route to account-linking routes\n- `integrations/` - provider adapters and ingest pipelines\n- `schemas/` - Zod route schemas\n- `types/` - TypeScript declarations, including `Express.Request` augmentation\n- `db/` - pool management, grants, migrations, and RLS policies\n- `config/` - logging and Swagger config\n- `utils/` - startup helpers, CORS, permissions, timezone loading, OIDC helpers, migration helpers\n- `ai/` - AI provider configuration (`config.ts`), the unified provider-dispatch helper (`providerDispatch.ts`), and the in-process chatbot tool registry (`ai/tools/`)\n- `security/` - encryption utilities (`encryption.ts`)\n- `validation/` - legacy express-validator rules for a few older routes (new routes use Zod schemas)\n- `constants/` - shared constants and supporting package data\n- `tests/` - Vitest suites plus a few utility scripts\n- `devdocs/` - local notes and debugging artifacts when present\n\nWhen searching, ignore noisy/generated directories unless you explicitly need them:\n\n- `node_modules/`\n- `coverage/`\n- `uploads/`\n- `temp_uploads/`\n- `backup/`\n- `mock_data/`\n\n## Architecture\n\n### Boot and App Shell\n\n- `index.ts` is the true local boot path used by `pnpm start`; do not bypass it for normal development because it performs env loading and preflight work\n- `SparkyFitnessServer.ts` creates the Express app, configures static upload serving, mounts auth interception, registers routes, exposes API docs, starts background jobs through `services/backgroundJobScheduler.ts`, and handles graceful shutdown\n- Startup order matters:\n  - `index.ts`: await `initializeDatabase()`, which applies pending migrations and then reapplies `db/rls_policies.sql` under a PostgreSQL advisory lock, **before** importing `SparkyFitnessServer.ts` (and therefore `auth.ts`)\n  - upsert env-configured OIDC provider\n  - mount Better Auth\n  - sync trusted SSO providers\n  - register cron jobs\n  - optionally promote `SPARKY_FITNESS_ADMIN_EMAIL` to admin\n  - start listening\n- Public API docs live at:\n  - `/api/api-docs/swagger`\n  - `/api/api-docs/redoc`\n  - `/api/api-docs/json`\n- If you change public endpoints, keep Swagger JSDoc and `config/swagger.ts` coverage accurate\n\n### Environment and Secrets\n\n- Runtime `.env` is expected at `../.env`\n- The tracked template lives at `../docker/.env.example`\n- `utils/secretLoader.ts` loads `*_FILE` secrets before preflight validation\n- Three layers decide whether a variable has to be set, and they are easy to confuse:\n  1. **`utils/preflightChecks.ts` refuses to start** without these four, because none has a safe default:\n     - `SPARKY_FITNESS_DB_PASSWORD`\n     - `SPARKY_FITNESS_FRONTEND_URL`\n     - `SPARKY_FITNESS_API_ENCRYPTION_KEY`\n     - `BETTER_AUTH_SECRET`\n  2. **`preflightChecks.ts` fills in a default** for `SPARKY_FITNESS_DB_HOST` (`sparkyfitness-db`), `SPARKY_FITNESS_DB_NAME` (`sparkyfitness_db`), `SPARKY_FITNESS_DB_USER` (`sparky`) and the two app-role variables, logging which one it defaulted. These matter only outside Compose, which supplies them itself.\n  3. **`docker/docker-compose.prod.yml` supplies a value** for almost everything via `${VAR:-default}`, so a Compose deployment only ever has to set the four in (1). Keep the defaults in (2) identical to Compose's: a value that differs between them silently points the server at a database other than the one Compose created.\n- `SPARKY_FITNESS_APP_DB_USER` and `SPARKY_FITNESS_APP_DB_PASSWORD` are soft-required: preflight defaults the user to `sparky_app` and mints a password when absent, and `utils/dbMigrations.ts` creates the role or re-syncs its password so the two always match. It probes a connection as that role first, so an externally pre-created role is left alone and the owner does not need `CREATEROLE` — but only while `SPARKY_FITNESS_APP_DB_PASSWORD` still authenticates. If it is absent, preflight mints a new one, the probe fails, and the `ALTER ROLE` does need `CREATEROLE`; an externally managed database should therefore set both app variables explicitly. The probe only reports failure for an authentication rejection (`28P01`/`28000`); any other connection error propagates rather than being misread as a stale password. Both assignments must stay in `preflightChecks.ts`, because `db/poolManager.ts` freezes its credentials at module load\n- `BETTER_AUTH_SECRET` is mandatory. It signs session cookies and encrypts stored 2FA/TOTP secrets, so a value that changes between restarts logs every user out and permanently locks out anyone with 2FA enabled. Startup used to mint a throwaway one when it was missing, which made exactly that happen silently; it now fails preflight instead\n- Preflight also refuses to start when `BETTER_AUTH_SECRET` or `SPARKY_FITNESS_API_ENCRYPTION_KEY` still holds a template placeholder (a value starting with `changeme` or `replace_with`, as shipped in `docker/.env.example` and `docker/.env.simple.example`), and only warns for `SPARKY_FITNESS_DB_PASSWORD` because Compose initialises Postgres with it. Keep new template secrets on one of those prefixes so the check covers them. It also fails when `BETTER_AUTH_SECRET` base64-decodes to an empty key (Better Auth accepts an empty Buffer) and warns under 32 decoded bytes\n- Common operational toggles include `SPARKY_FITNESS_SERVER_PORT`, `SPARKY_FITNESS_ADMIN_EMAIL`, `ALLOW_PRIVATE_NETWORK_CORS`, `ALLOW_PRIVATE_NETWORK_AI`, `ALLOW_PRIVATE_NETWORK_FOOD_PROVIDERS`, `SPARKY_FITNESS_EXTRA_TRUSTED_ORIGINS`, and `BETTER_AUTH_URL`\n- User-configured self-hosted food providers (Mealie/Tandoor/Norish) can point `base_url` at a private/internal address only for admins by default; a non-admin on a multi-user server is blocked unless the operator opts in, either with the admin `allow_private_network_food_providers` toggle (Admin > Global Provider Settings) or `ALLOW_PRIVATE_NETWORK_FOOD_PROVIDERS=true`. This mirrors the AI policy (a single-user self-host is an admin, so their LAN recipe server works with no config). Enforced by `utils/outboundUrlPolicy.ts` at provider save time in `services/externalProviderService.ts`. Separate from the AI toggle by design\n- The admin `allow_private_network_ai` toggle (Admin > Global AI Settings), or `ALLOW_PRIVATE_NETWORK_AI=true`, lets non-admin users use custom AI service URLs (`custom`/`ollama`/`openai_compatible`) that resolve to private/internal addresses; default off is an SSRF guard enforced by `utils/outboundUrlPolicy.ts` at save/test time and again in the runtime guarded fetch path. Current admins and global admin-created AI settings can use private URLs for self-hosted providers like Ollama\n- **Call `resolveAiNetworkPolicy` / `resolveFoodProviderNetworkPolicy`, not the `derive*` forms.** The sync `derive*` functions only see the env var; the async `resolve*` wrappers also consult the admin toggle (and only hit the database when the sync answer would be a denial). The `derive*` exports stay for unit tests and for the resolvers themselves\n\n### TypeScript and Module Conventions\n\n- This package is now almost entirely TypeScript; new source files should be `.ts`\n- Keep local relative imports using `.js` extensions from TypeScript files, for example `import foo from './foo.js'`\n- `eslint.config.js` enforces file extensions in imports\n- `tsconfig.json` uses `NodeNext`, `noEmit`, and `allowJs: false`\n- `@workspace/shared` resolves directly to `../shared/src/index.ts` here and in Vitest\n- Avoid using `any` declarations in models, repositories, and integration services (e.g. `integrations/fatsecret/fatsecretService.ts`). Instead, use base datatypes (like `string`), proper types/interfaces, or import strict type schemas directly from `@workspace/shared`.\n- New public endpoints should include TypeScript code, Zod validation, and automated tests\n\n### Logging\n\n- Use `log(level, message, ...args)` from `config/logging.ts`; levels are `'debug'`, `'info'`, `'warn'`, and `'error'`\n- Never use `console.error` (or other `console.*`) in application code\n- Exception: fatal boot diagnostics in `index.ts` and `utils/preflightChecks.ts` print with `console.error` (alongside `log('error', ...)`), because `log()` is suppressed at `SILENT` and the operator must still see why the server refused to start\n- `SPARKY_FITNESS_LOG_LEVEL` controls verbosity (`DEBUG`, `INFO`, `WARN`, `ERROR`, `SILENT`)\n\n### Database and RLS\n\n- Use `getClient(userId, authenticatedUserId?)` from `db/poolManager.ts` for normal user-scoped queries\n- `getClient(...)` sets `public.set_app_context(...)`; that is what makes row-level security work correctly\n- Use `getSystemClient()` only for admin, migration, startup, or policy-management work that intentionally bypasses RLS\n- Always release database clients in a `finally` block\n- To learn a table's current shape, read `../shared/src/schemas/database/<Table>.zod.ts` (one small Zod file per table) instead of reading `../db_schema_backup.sql` or reconstructing it from the 185 migration files\n- New migrations belong in `db/migrations/` and must use `YYYYMMDDHHMMSS_description.sql`\n- **Never manually edit `../db_schema_backup.sql`** — after merge, CI regenerates it from the migrations and opens an automated sync PR (`.github/workflows/schema-backup.yml`). Do not commit copies generated from a local database.\n- If you add a new table or change user-visible access behavior, follow `../agent-docs/new-migration-checklist.md`. In short, you MUST:\n  1. Add/modify the RLS policies in `db/rls_policies.sql`.\n  2. Update the user-facing documentation in `../docs/src/features/family-friends-sharing.md`.\n  3. Update the developer-facing documentation in `../docs/src/developer/database-security-tiers.md` to define its security tier (Tier 1, Tier 2, or Tier 3).\n  4. Add or update the matching Zod schema in `../shared/src/schemas/database/`.\n- Keep future schema-startup steps in `utils/initializeDatabase.ts` and pass its shared client through all database work. The lock and schema work must use the same connection so initialization cannot continue on another connection after the lock-owning session is lost. Do not create alternate migration mechanisms.\n- Migrations run from `index.ts`, **before any application module is imported**, and via dynamic `await import()`. Both details are load-bearing: Better Auth validates the schema eagerly at `auth.ts` module scope and caches a mismatch for the life of the process (issues #2469 / #2470), and `db/poolManager.ts` builds its pools at module load, so a static import would be hoisted above the env/secret loading. `tests/bootOrder.test.ts` guards this\n\n### Uploads: Public vs Sensitive\n\n- `SparkyFitnessServer.ts` serves the uploads root publicly at `/uploads` and `/api/uploads`; both are in `publicRoutes`, so `authenticate` never runs on them\n- Sensitive subtrees are **denied on the static mount** and served instead by an authenticated, owner-checked per-id route. Three exist today:\n  - `check-in` -> `GET /api/measurements/check-in-photos/file/:id` (delegatable via the `checkin` permission)\n  - `pregnancy` -> `GET /api/v2/pregnancy/photos/file/:id` (owner-only; deliberately **no** `checkPermissionMiddleware`, because reproductive-health data is never delegated)\n  - `symptoms` -> `GET /api/v2/symptoms/photos/file/:id` (delegatable via the `symptoms` permission)\n- Adding a sensitive upload subtree means adding its directory name to `SENSITIVE_UPLOAD_SUBTREES` in `SparkyFitnessServer.ts` **and** adding an authenticated file route; the deny rule matches the decoded, normalized path, because a prefix match on the raw URL is bypassable with `..%2f`\n- Responses for these domains omit `file_path`: the on-disk layout is a server detail and clients address photos by id\n- `tests/uploadsStaticMount.test.ts` guards both the deny behavior and the fact that the deny rule is registered before `express.static`\n\n### Auth and Request Context\n\n- Better Auth is configured in `auth.ts` and mounted under `/api/auth`\n- `SparkyFitnessServer.ts` intercepts `/api/auth*` requests before the normal request logger and has special handling for discovery routes and sign-out cookie cleanup\n- `middleware/authMiddleware.ts` populates:\n  - `req.userId`\n  - `req.authenticatedUserId`\n  - `req.originalUserId`\n  - `req.activeUserId`\n  - `req.user`\n- `req.userId` is the active RLS target; `req.authenticatedUserId` is the logged-in actor\n- Family and delegated access flow through `middleware/checkPermissionMiddleware.ts`, `middleware/onBehalfOfMiddleware.ts`, and the auth middleware’s active-user switching\n- `checkPermissionMiddleware(permissionType)` guards routes; permission types are `'diary'`, `'reports'`, `'checkin'`, `'medications'`, and `'symptoms'` (GET resolves to the read-only `*_read` variant)\n- If you change auth behavior, check both cookie-backed sessions and API key flows\n- API keys carry a scope in `api_key.permissions` (Better Auth format, `@workspace/shared` `ApiKeys.api.zod.ts`): `{ sparky: [\"read\"] }` is read-only, `{ sparky: [\"read\", \"write\"] }` or no permissions is full access. The scope is set only by `POST /api/identity/user/generate-api-key`, because Better Auth refuses `permissions` from a client. `authenticate` sets `req.apiKeyReadOnly` and answers 403 for anything but GET/HEAD/OPTIONS plus the POST allowlist in `utils/apiKeyScope.ts`; the `/api/auth` interceptor refuses mutations from read-only keys via `middleware/readOnlyApiKeyGuard.ts`; `/mcp` filters tools and actions with `ai/mcp/toolAccess.ts`. A new MCP tool or action is unavailable to read-only keys until it is classified there\n\n### Dates, Day Strings, and Timezones\n\n- Prefer the shared helpers exported by `@workspace/shared` for day-string and timezone-aware logic\n- Common server-side helpers include `todayInZone`, `instantToDay`, `instantForDay`, `dayToUtcRange`, `dayRangeToUtcRange`, `localDateToDay`, `addDays`, `compareDays`, and `isDayString`\n- Load the user timezone through `utils/timezoneLoader.ts` before deriving \"today\", bucketing events by day, or building date ranges from user context\n- Treat `YYYY-MM-DD` values as calendar-day strings, not UTC-midnight timestamps\n- Avoid `toISOString().split('T')[0]` for user-facing or business-logic dates; it silently shifts dates near timezone boundaries\n- If you touch older code that still uses UTC split patterns, prefer migrating that path to the shared helpers instead of copying the pattern forward\n- Timezone/date regression coverage already exists in:\n  - `tests/timezone.test.ts`\n  - `tests/dateShifting.test.ts`\n  - `tests/measurementService.timezone.test.ts`\n\n### Integrations and Background Work\n\n- Provider-specific adapters live under `integrations/`; coordinating logic usually lives in `services/` and persistence in `models/`\n- Current adapters span food/nutrition (OpenFoodFacts, FatSecret, Nutritionix, USDA, Mealie, Tandoor, Norish, SwissFood, Yazio), fitness devices (Garmin Connect sync plus FIT file import via `integrations/garminfit/` + `services/fitImportService.ts`, Withings, Fitbit, Oura, Polar, Strava, COROS, Hevy), exercise databases (Wger, FreeExerciseDB), and health-data import (Google Health, generic/mobile health data)\n- Scheduled jobs currently include backups, session cleanup, and hourly sync loops for Withings, Garmin, Fitbit, Oura, Polar, Strava, and COROS\n- Integration work often spans route, service, repository, cron, and external-provider settings code; inspect the whole path before calling the work complete\n- **OAuth linking (`/authorize`, `/callback`) is self-only, and `state` is a server-issued single-use nonce.** Never derive a user id from a callback request body, and never gate an authorize route with `checkPermissionMiddleware('diary')` — on GET that resolves to `diary_read`, which would hand a read-only delegate the owner's decrypted OAuth client id. Use `requireSelfActor` plus `utils/oauthState.ts`. Withings and Polar follow this pattern; Oura, Fitbit and Strava are self-only but still send `state = userId` and ignore it on callback (tracked follow-up)\n\n### AI Services\n\n- AI calls go through the Vercel `ai` SDK (v7) with provider adapters for OpenAI, Anthropic, and Google, plus OpenAI-compatible, Mistral, Groq, OpenRouter, and Ollama service types\n- `ai/config.ts` holds default model and vision-model selection per provider; `ai/providerDispatch.ts` is the unified dispatch helper used by chat, food-photo analysis, nutrition-label scan, and unit conversion\n- Prefer routing new AI features through `providerDispatch.ts` instead of calling provider SDKs directly\n- Web and mobile voice transcription use `POST /api/chat/transcribe` in `routes/chatRoutes.ts` and `services/voiceTranscriptionService.ts`; its multipart fields, response, MIME types, and 10 MB limit come from `shared/src/schemas/api/VoiceTranscription.api.zod.ts`. Audio stays in memory, uses actor-scoped AI settings with `supports_audio_input`, and all outbound calls use `createGuardedFetch`. Voice and chat providers are selected independently; transcription never executes chatbot tools or writes diary entries. `voice_input_enabled` and `active_voice_ai_service_id` in user preferences synchronize voice UI across clients; null selects System recognition, and disabling voice preserves the selection. Preference writes validate these fields using the shared voice-preferences schema.\n- Chatbot tool calls run in-process through the registry in `ai/tools/`\n- `ai/tools/index.ts` exposes `buildChatbotTools(userId, tz)`, composing the per-domain builders (`build<Domain>Tools` in `ai/tools/<domain>Tools.ts`); handlers close over the authenticated user — so two-actor services receive `(userId, userId, ...)` — and the user's IANA timezone, used for \"today\" defaults and day bucketing\n- Tool handlers follow a fixed contract: publish a flat Zod schema, validate with a strict union `safeParse` inside `execute`, orchestrate through existing services and repositories, and never throw - errors come back as `ERRORS.*` strings from `ai/tools/errors.ts`\n- Tool output text is a parity contract with the MCP tool set; golden tests in `tests/chatbotTools*.test.ts` assert exact returned strings, so do not reword tool output casually\n\n## Testing and Validation\n\n- Test runner: Vitest, not Jest\n- Auto-discovered test files match `tests/**/*.test.ts`\n- `tests/check_routes.ts` and `tests/*.script.ts` are utility scripts, not normal test suites\n- For route or contract work, targeted `supertest`-based Vitest tests are the normal validation path\n- Prefer `pnpm run typecheck` after touching `routes/v2/`, `schemas/`, `types/`, or shared request/response contracts\n- Prefer `pnpm run lint` after multi-file edits; if unrelated package-wide issues make that noisy, run targeted `pnpm exec eslint <paths>` on the touched files before stopping\n- Use `pnpm run test:coverage` after broad service, route, repository, middleware, or auth refactors\n\n## Quick Routing\n\n- Weekly weight-change calorie goals: `services/preferenceService.ts` validates the signed `goal_mode_weekly_change_kg` preference (negative loses, positive gains; active only for `goal_mode = 'weekly'`), `models/preferenceRepository.ts` persists it, and `services/goalService.ts` plus `services/calorieBalanceService.ts` pass it to the shared calorie adjustment functions. Device Projection applies the daily weekly-rate adjustment once to device TDEE; percentage goals keep their existing behavior.\n\n- Startup, env, or deployment issue:\n  inspect `index.ts`, `SparkyFitnessServer.ts`, `utils/secretLoader.ts`, `utils/preflightChecks.ts`, and `config/logging.ts`\n- Auth, session, MFA, or API key issue:\n  inspect `auth.ts`, `middleware/authMiddleware.ts`, `routes/authRoutes.ts`, and `routes/auth/`\n- Migration, RLS, or permission issue:\n  inspect `db/migrations/`, `db/rls_policies.sql`, `db/poolManager.ts`, `utils/applyRlsPolicies.ts`, and the permission middleware/helpers\n- Public v2 contract issue:\n  inspect the matching file in `routes/v2/` plus the related Zod schema in `schemas/`\n- Food, barcode, or external provider issue:\n  inspect the relevant `integrations/*` code, then the matching service and repository files\n- Open Food Facts publication:\n  inspect `services/openFoodFactsManualContributionService.ts`, `integrations/openfoodfacts/openFoodFactsContribution.ts`, and `constants/openFoodFacts.ts`; retained automatic queue code is dormant and needs a new migration before a future release can activate its triggers\n- Heart rate zone, max heart rate or time-in-zone issue:\n  inspect `../shared/src/utils/heartRateZones.ts`, `services/heartRateZoneService.ts`, `routes/v2/heartRateZoneRoutes.ts`, then `services/healthDataHandlers.ts` (`persistWorkoutTelemetry`) and `services/exerciseEntryService.ts` (watch telemetry) for the write paths\n- Health data or date bucketing issue:\n  inspect `integrations/healthData/healthDataRoutes.ts`, `services/measurementService.ts`, and `utils/timezoneLoader.ts`\n- Attaching heart rate to an already-existing exercise entry (e.g. from a paired Apple Watch's live workout tracking, `SparkyFitnessMobile/src/hooks/useWatchWorkoutBridge.ts`):\n  inspect `POST /exercise-entries/:id/watch-telemetry` in `routes/exerciseEntryRoutes.ts`, `services/exerciseEntryService.ts`'s `attachWatchTelemetryToExerciseEntry`, and `models/exerciseEntry.ts`'s `applyWatchTelemetryAtomically`. Distinct from `services/healthDataHandlers.ts`'s `persistWorkoutTelemetry`, which creates a new entry as part of importing a whole synced workout (HealthKit/Health Connect/Garmin) — this route only fills in avg/max HR, `exercise_entry_hr_zones` and `calories_burned` on an entry that already exists, and it also stores the posted series as `heart_rate` samples tagged with the entry (`storeHeartRateSeries`, source `SparkyFitness Watch`, `merge` over the workout's own span but only this entry's samples, and only after that snapshot was accepted under the row lock, best effort) because the heart rate chart of an entry with no GPS track reads those samples and would otherwise wait on a Health sync. `activeEnergyKcal` is a real measurement from the watch and is written to BOTH `calories_burned` (the figure the diary totals) and `active_calories` (a telemetry column the ordinary entry update preserves). That second write is what makes the measurement survive a later edit: `resolveEditedCaloriesBurned` in `services/exerciseService.ts` prefers a stored `active_calories` over the recomputed duration-and-sets estimate, so editing a note or a weight no longer replaces what a watch measured with a formula. A client-sent `calories_burned` still overrides both. Both body fields are individually optional (HealthKit permissions are per type) and the model does a partial UPDATE, so a calories-only post must not blank heart rate an earlier post attached.\n  Workout effort (a 1-10 rating of the whole workout, `exercise_entries.workout_effort`, CHECK 1-10): it is one of `EXERCISE_ENTRY_TELEMETRY_COLUMNS`, so a synced workout carries it as `telemetry.workout_effort` through the health-data allowlist (HealthKit sends the person's own Apple rating) and the model's insert, update and select pick it up with no per-column code. `POST /exercise-entries/:id/effort` (`setExerciseEntryEffort`, body `{rating}`) is the write for ratings given afterwards, on the watch: it sets the entry and every other entry with the same `exercise_preset_entry_id`, so a strength session reads the same from any exercise and the phone needs only one entry id. `GET /api/exercise-stats/query` returns it as `workoutEffort`.\n- Attaching a phone-recorded GPS track to an existing exercise entry (mobile `RecordActivityScreen`):\n  inspect `POST /exercise-entries/:id/gps-track` in `routes/exerciseEntryRoutes.ts` and `services/exerciseEntryService.ts`'s `attachGpsTrackToExerciseEntry`, with the request schema `attachExerciseEntryGpsTrackRequestSchema` in `shared/src/schemas/api/ExerciseEntries.api.zod.ts`. It stores the route and laps (laps arrive as bare windows and are derived by `deriveLaps`) and backfills speed/elevation/moving-time with `deriveWorkoutTelemetry`, all in one transaction. Both inserts upsert per entry, so a retried post replaces the track. Use this rather than `/api/health-data` for app-recorded activities: the health-data path range-deletes a source's entries for the affected days.\n- Attaching laps to an entry with no GPS track (an indoor run recorded on the watch):\n  inspect `POST /exercise-entries/:id/laps` in `routes/exerciseEntryRoutes.ts` and `services/exerciseEntryService.ts`'s `attachLapsToExerciseEntry`, with `attachExerciseEntryLapsRequestSchema` in `shared/src/schemas/api/ExerciseEntries.api.zod.ts`. The client sends the marked windows with the distance and average heart rate it worked out; the server derives duration and speed and upserts by lap index into `exercise_entry_laps`, so a retry replaces them. Entries with a track get their laps from `/gps-track` instead.\n  For an indoor run's pace chart, `POST`/`GET /exercise-entries/:id/distance-series` (`attachDistanceSeriesToExerciseEntry`, `getExerciseEntryDistanceSeries`, table `exercise_entry_distance_series`, rows of `[active seconds, metres]`; `setExerciseEntryDistance` rescales them with the laps).\n- Correcting the distance of an entry with no GPS track (a treadmill run, from the summary screen):\n  inspect `POST /exercise-entries/:id/distance` in `routes/exerciseEntryRoutes.ts`, `services/exerciseEntryService.ts`'s `setExerciseEntryDistance` and `setExerciseEntryDistanceRequestSchema` in `shared/src/schemas/api/ExerciseEntries.api.zod.ts`. It changes only the entry's `distance`, its sets' distance and any laps (`distance_meters` and speed scaled by new over old distance), in one transaction, so measured heart rate and energy are kept; an entry with a GPS track is refused with 409.\n- Water, hydration, caffeine, or alcohol issue:\n  inspect `services/hydrationTotalsService.ts` (the single owner of the daily water formula), `services/measurementService.ts` (the container \"+/-\" path and the container->food link), `services/caffeineKineticsService.ts` / `services/alcoholWeekService.ts`, `models/waterContainerRepository.ts`, and the shared maths in `../shared/src/nutrients/`\n- Self-service \"delete synced data by source\" issue:\n  inspect `routes/syncedDataRoutes.ts`, `services/syncedDataService.ts`, and `models/syncedDataRepository.ts` (the `SYNCED_SOURCE_TABLES` whitelist)\n- AI chat or chatbot tool issue:\n  inspect `services/chatService.ts`, `ai/tools/`, and the matching domain service and repository\n- Fasting or mood issue:\n  inspect `routes/fastingRoutes.ts` / `routes/moodRoutes.ts` and `models/fastingRepository.ts` / `models/moodRepository.ts`\n  `mood_entries` holds several rows per day, each with `entry_timestamp` and a `source`: `manual` (web/app check-ins, AI `log_mood`, end-of-fast mood), or `garmin` / `import` (derived, one row per source per day via a partial unique index). `mood_value` is nullable; a CHECK needs at least one of mood, energy, focus, notes or tags (whitespace-only notes and blank tags do not count, as in `hasMoodContent`), and another limits `source` to those three values. Requests without the `X-Mood-Model-Version` header keep the one-mood-per-day contract on the \"legacy row\" (latest manual row with a mood, falling back to the derived row on GET, Garmin before import); `X-Mood-Model-Version: 2` always adds a check-in and reads arrays (header is in `CORS_ALLOWED_HEADERS`, `utils/corsHelper.ts`); its PUT edits only manual rows (409 for Garmin and import) and re-dates a check-in only when its timestamp actually changes. A check-in or derived mood written without a time of its own gets `instantForDay` from `@workspace/shared`: now for today, local noon for another day, so `entry_timestamp` stays on `entry_date`. Per-day aggregates (coach, cycle correlations) use `DAILY_MOOD_VALUE_SQL`: rounded average of manual moods, else Garmin, else import.\n- Symptom or episode tracking issue (#1882):\n  inspect `routes/v2/symptomRoutes.ts`, `services/symptomService.ts`, `models/symptomRepository.ts`, `models/symptomOptionRepository.ts`, and the shared contract and built-ins in `../shared/src/schemas/api/Symptoms.api.zod.ts` and `../shared/src/symptoms/`. `symptom_entries` also holds cycle-hub symptoms (`source = 'cycle'`), which RLS keeps owner-only\n- Medications, cycle, or pregnancy issue:\n  inspect the matching v2 route (`routes/v2/medicationRoutes.ts`, `routes/v2/cycleRoutes.ts`, `routes/v2/pregnancyRoutes.ts`), its Zod schema in `schemas/`, then `services/cycleService.ts` / `services/pregnancyService.ts` and the `models/medication*Repository.ts` / `models/cycleRepository.ts` / `models/pregnancyRepository.ts` files\n- Supplement barcode lookup:\n  `GET /api/v2/medications/supplement-lookup?upc=` (registered before `/:id`) runs only while the user can see an active `dsld` or `openfoodfacts` external provider (`getActiveProvidersByTypes`; neither is a 404; the NIH database answers first and `services/supplementOpenFoodFactsService.ts` asks Open Food Facts, per-serving nutrients only, when it has no match or `dsld` is off; migration `20261006200000_add_dsld_provider_type.sql`, constant `SUPPLEMENT_LOOKUP_PROVIDER_TYPE`) and calls `services/supplementLookupService.ts`. Two more routes read a Supplement Facts photo: `POST /supplement-label/scan` runs the user's vision AI (`services/supplementLabelScanService.ts`, same provider and error map as `/foods/scan-label`) and `POST /supplement-label/map` takes ingredients the phone read on device; both end in `mapScannedLabel`, so name matching and unit conversion are shared with the barcode lookup, which searches the NIH Dietary Supplement Label Database for the code, opens the hits to check the label's own UPC, and maps the label to one serving's nutrients in the app's units (IU for vitamins A, D and E converted, compounds and unplaceable ingredients returned as `unmatched`). The response contract is `shared/src/schemas/api/SupplementLookup.api.zod.ts`; an unreachable database is a 502, no match is `{ product: null }`.\n- Exercise alternatives, workout feedback, or adaptive suggestions issue (#1560):\n  inspect `services/exerciseAlternativesService.ts` (library + Free Exercise DB candidates, dedupe) with the pure ranking in `utils/exerciseAlternativesRanking.ts` and the muscle/equipment vocabulary in `../shared/src/constants/exerciseTaxonomy.ts`; feedback in `services/workoutCoachingService.ts` + `models/workoutFeedbackRepository.ts`; signals in `services/adaptiveWorkoutService.ts`. The rules that turn signals into weight changes are client-side and shared (`../shared/src/utils/adaptiveCoaching.ts`) so web, mobile and the AI tools agree; the server only reports what happened. AI actions: `suggest_alternatives`, `rate_workout`, `get_workout_coaching` in `ai/tools/exerciseTools.ts`\n- Bodyweight exercise load, volume or 1RM issue (#56):\n  a `bodyweight_reps` set's weight is signed added/assisting load, and its load is body weight plus that weight (`effectiveLoadKg` in `../shared/src/utils/exerciseLoad.ts`). SQL mirrors the rule in `utils/exerciseLoadSql.ts` (`bodyWeightJoinSql`, `setLoadSql`), used by `services/exerciseStatsService.ts` and `models/exerciseEntry.ts`'s progress query; `services/reportService.ts` applies the shared helper with readings from `reportRepository.getBodyWeightReadings`. Keep the two copies identical. A negative set weight is rejected unless that exercise's modality is `bodyweight_reps`. `models/exercise.ts`'s `createExercise` defaults the modality from equipment through `resolveExerciseModality`\n  `weight_distance` (weighted carries) and `weight_duration` (loaded holds) store weight plus distance (km) or duration (s); stats treat them as weight-based, not cardio, and best-set ordering falls back to distance then duration.\n- Sleep or sleep-science issue:\n  inspect `routes/sleepRoutes.ts`, `routes/sleepScienceRoutes.ts`, `services/sleepAnalyticsService.ts`, `services/sleepScienceService.ts`, and the sleep repositories\n\n## Architecture Resources\n\nBefore adding a feature or changing auth/permission behavior, read:\n\n- `../docs/src/developer/database.md` — Quick table index (all ~120 tables with purpose) + migration best practices\n- `../docs/src/developer/database-security-tiers.md` — Security tier, permission type, and RLS rules for every table (authoritative)\n- `../agent-docs/architecture-permissions.md` — Permission types, links to tier classification doc\n- `../agent-docs/data-flow-patterns.md` — Data flow from frontend through server to database, safe RLS patterns\n- `../agent-docs/new-domain-template.md` — Checklist for adding a major feature domain\n- `../agent-docs/anti-patterns.md` — Common mistakes (using getSystemClient(), forgetting RLS, cache invalidation, timezone bugs, cross-package contract mismatches)\n\n## Working Rules\n\n- Match the existing service/repository/middleware layering instead of introducing parallel abstractions\n- **Library Deletes vs Diary Snapshots:** `exercise_entries` and `food_entries` are snapshot-backed (`exercise_id` / `food_id` are `ON DELETE SET NULL`). `deleteExercise` and `deleteFood` (`mode: 'delete'`) must never delete past or today's diary entries; they cascade from templates/presets, clean up future scheduled plan entries (`entry_date >= today AND workout_plan_assignment_id IS NOT NULL`), and clean up empty parent preset entries. Only explicit `delete_with_history` (force delete) deletes diary entries for that user. If an item is referenced by others (`otherUserReferences > 0`), the delete must fall back to `hide` (`is_quick_exercise` / `is_quick_food`).\n- If your change adds a new domain, route family, or table, update this file's Snapshot, Source Map, and Quick Routing sections (and the `Last updated` date) in the same change\n- If you add persisted or user-visible data, think through migration, RLS, permissions, tests, API docs, and downstream client contracts together\n- Validate shared-contract\n\n<!-- File: .claude/skills/new-migration/SKILL.md (Tokens: ~115 | Category: anthropic-skill) -->\n---\nname: new-migration\ndescription: Use whenever adding or changing a SparkyFitness database migration, creating a new table, or altering user-visible data access. Walks the mandatory cross-package checklist (RLS policies, schema backup sync, shared Zod schema, docs security tiers, downstream contracts). Triggers on \"new table\", \"migration\", \"ALTER TABLE\", \"RLS\", \"schema change\".\n---\n\n# New Migration\n\nSee @.agents/skills/new-migration/SKILL.md — that is the canonical skill body. Read it and follow it.\n\n<!-- File: CLAUDE.md (Tokens: ~6 | Category: claude-rule) -->\nSee @AGENTS.md\n\n<!-- File: docs/CLAUDE.md (Tokens: ~51 | Category: claude-rule) -->\n# CLAUDE.md\n\nThis is the SparkyFitness documentation site built with **VitePress**. For work on docs, inspect `package.json` scripts and the `src/` layout directly (`.vitepress/config.mts`).\n\n<!-- File: shared/CLAUDE.md (Tokens: ~12 | Category: claude-rule) -->\n# CLAUDE.md\n\nSee @AGENTS.md\n\n<!-- File: SparkyFitnessFrontend/CLAUDE.md (Tokens: ~12 | Category: claude-rule) -->\n# CLAUDE.md\n\nSee @AGENTS.md\n\n<!-- File: SparkyFitnessMobile/CLAUDE.md (Tokens: ~6 | Category: claude-rule) -->\nSee @AGENTS.md\n\n<!-- File: SparkyFitnessServer/CLAUDE.md (Tokens: ~300 | Category: claude-rule) -->\n# SparkyFitnessServer - Claude Agent Guidelines & System Prompt\n\nPath: `SparkyFitnessServer/CLAUDE.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/CodeWithCJ/SparkyFitness/HEAD/SparkyFitnessServer/CLAUDE.md)\n\n<!-- File: .github/copilot-instructions.md (Tokens: ~1637 | Category: copilot-instructions) -->\n# SparkyFitness AI Coding Agent Instructions\n\n## Architecture Overview\n\n**Full-stack fitness tracking application** - self-hosted alternative to MyFitnessPal with AI-powered nutrition assistance.\n\n- **Frontend**: React 18 + TypeScript + Vite (`src/`)\n- **Backend**: Node.js/Express (`SparkyFitnessServer/`)\n- **Database**: PostgreSQL with Row Level Security (RLS)\n- **UI**: Tailwind CSS + shadcn/ui components\n- **State**: React Context + TanStack Query\n- **AI**: Multi-provider support (OpenAI, Anthropic, Google, etc.)\n\n## Development Workflow\n\n### Running the Application\n```bash\n# Frontend development server (port 8080)\nnpm run dev\n\n# Backend server (port 3010)  \nnpm run start-backend\n\n# Docker development (local builds)\n./docker/docker-helper.sh dev up\n\n# Docker production (DockerHub images)\n./docker/docker-helper.sh prod up\n```\n\n### Key Configuration Files\n- **Vite config** (`vite.config.ts`): API proxy setup (`/api` → `localhost:3010`)\n- **Environment**: Root `.env` file (loaded by backend at `path.resolve(__dirname, '../.env')`)\n- **Docker**: `docker/` directory contains organized Docker configuration\n  - `docker-compose.dev.yml` - Local development builds with volume mounts\n  - `docker-compose.prod.yml` - Production DockerHub images\n  - `docker-helper.sh` - Comprehensive management script with help system\n  - `nginx.conf.template` - Parameterized nginx configuration with envsubst\n  - `.env.example` - Template for environment variables\n\n### Docker Helper Script\nThe `docker-helper.sh` script provides easy management:\n```bash\n# Show all available commands\n./docker/docker-helper.sh help\n\n# Development environment\n./docker/docker-helper.sh dev up     # Start all services\n./docker/docker-helper.sh dev logs   # View logs\n./docker/docker-helper.sh dev clean  # Clean volumes and images\n\n# Production environment  \n./docker/docker-helper.sh prod up    # Start production services\n./docker/docker-helper.sh prod down  # Stop and remove containers\n```\n\n## Critical Architecture Patterns\n\n### Repository Pattern (Backend)\nAll database operations use repository pattern in `SparkyFitnessServer/models/`:\n```javascript\n// Example: userRepository.js\nconst pool = require('../db/connection');\n\nasync function createUser(userId, email, hashedPassword, full_name) {\n  const client = await pool.connect();\n  try {\n    await client.query('BEGIN');\n    // Multiple related inserts in transaction\n    await client.query('COMMIT');\n  } catch (error) {\n    await client.query('ROLLBACK');\n    throw error;\n  } finally {\n    client.release();\n  }\n}\n```\n\n### External Provider Integration\nModular integration system in `SparkyFitnessServer/integrations/`:\n- **Food providers**: OpenFoodFacts, Nutritionix, FatSecret\n- **Exercise data**: Wger integration\n- **Health data**: Apple Health integration\n- **Encryption**: All API keys encrypted at rest using `security/encryption.js`\n\n### Frontend Context Providers\nKey contexts in `src/contexts/`:\n- **PreferencesContext**: User settings, goals, theme, AI provider config\n- **ChatbotVisibilityContext**: AI assistant state management\n\n### Database Schema Conventions\n- **UUID primary keys** for all tables (`gen_random_uuid()`)\n- **Audit fields**: `created_at`, `updated_at` on all tables\n- **RLS policies**: Users only access their own data\n- **Transaction patterns**: Multi-table operations wrapped in transactions\n\n## AI Integration Patterns\n\n### Multi-Provider AI Support\n```javascript\n// SparkyFitnessServer/ai/config.js\nfunction getDefaultModel(serviceType) {\n  switch (serviceType) {\n    case 'openai': return 'gpt-4o-mini';\n    case 'anthropic': return 'claude-3-5-sonnet-20241022';\n    case 'google': return 'gemini-pro';\n    // ...\n  }\n}\n```\n\n### Chat System Architecture\n- **Backend**: `routes/chatRoutes.js` handles AI provider routing\n- **Frontend**: `DraggableChatbotButton` component for UI\n- **Storage**: Chat history with metadata in PostgreSQL\n- **Capabilities**: Food logging, image analysis, exercise tracking, measurements\n\n## Data Flow Patterns\n\n### Meal Planning System\n1. **Templates** (`meal_plan_templates`) → **Scheduled Plans** (`meal_plans`) → **Food Diary Entries** (`food_diary`)\n2. Complex meal expansion logic in `mealPlanTemplateRepository.js`\n3. Date-range planning support with single-day granularity\n\n### Family Access Control\n- **Granular permissions**: `calorie`, `checkin`, `reports`, `food_list`\n- **RLS enforcement**: Database-level security policies\n- **Access patterns**: `familyAccessRepository.js` manages relationship permissions\n\n### External API Error Handling\nConsistent error patterns across integrations:\n```javascript\ntry {\n  const response = await fetch(searchUrl, { method: 'GET' });\n  if (!response.ok) {\n    const errorText = await response.text();\n    log('error', \"API error:\", errorText);\n    throw new Error(`API error: ${errorText}`);\n  }\n  return await response.json();\n} catch (error) {\n  log('error', `Error with query \"${query}\":`, error);\n  throw error;\n}\n```\n\n## Development Conventions\n\n### API Response Patterns\n- **Success**: JSON data directly\n- **Error**: `{ error: \"message\" }` with appropriate HTTP status\n- **Auth**: JWT tokens in Authorization header\n- **CORS**: Configured for frontend URL in environment\n\n### Database Migration Strategy\n- **Auto-migrations**: `utils/dbMigrations.js` runs on server startup\n- **Version tracking**: Migrations applied based on version numbers\n- **Schema changes**: Always backward compatible during development\n\n### Component Architecture\n- **shadcn/ui** for base components\n- **Custom components** in `src/components/`\n- **Page-level** components in `src/pages/`\n- **Shared utilities** in `src/utils/`\n\n### Environment-Specific Behavior\n- **Development**: Vite proxy, hot reload, debug logging\n- **Docker**: Production builds, environment variable injection\n- **Database**: Connection pooling, transaction management\n\n## Key Files for Understanding\n- **Server entry**: `SparkyFitnessServer/SparkyFitnessServer.js` (route registration, middleware setup)\n- **Frontend entry**: `src/App.tsx` (context providers, query client setup)\n- **Documentation site**: `docs/` - Comprehensive Nuxt Content documentation (https://codewithcj.github.io/SparkyFitness)\n\n## Documentation Structure\n\n### Developer Documentation\nThe developer documentation has been consolidated into focused guides at `docs/content/3.developer/`:\n\n1. **`1.getting-started.md`** - Complete setup, installation, and quick start guide\n2. **`2.architecture.md`** - System architecture, patterns, and project structure  \n3. **`3.contributing.md`** - Development workflow, guidelines, and PR process\n4. **`4.database.md`** - Database schema, migrations, and operations\n5. **`5.api-reference.md`** - OpenAPI specification and endpoint documentation\n6. **`6.troubleshooting.md`** - Comprehensive debugging guide and common solutions\n\n### Documentation Site Features\n- **Live site**: https://codewithcj.github.io/SparkyFitness hosted on GitHub Pages\n- **Technology**: Nuxt Content + Docus theme for optimal developer experience\n- **Navigation**: Auto-generated navigation with numerical ordering\n- **Search**: Full-text search across all documentation\n- **Mobile-friendly**: Responsive design for all devices\n</yakaai_skills>"}