workout-app

CLAUDE.md — Repo Map for Future Sessions

Read this first, then go straight to the folder that owns the change. Don’t grep the whole repo. Each folder has a short README with file index + extension points.

What this app is

A native iOS 26 SwiftUI nutrition + workout journal, “Notes-app simple.” iOS-only. Local persistence via SwiftData; cloud sync via Supabase (per-user, RLS-scoped). Full product spec lives in ai_build_reference_health_workout_app.md.

Status: Phase 1.5 — UI is done in its current shape; backend schema is committed but not yet applied; iOS still runs MockFoodResolver. Next iterations wire Supabase Edge Function + Sign in with Apple, then cardio + progression engine.

App/WorkoutAppApp.swift launches JournalView directly inside a NavigationStack. There are no tabs and no custom floating bar — Apple-native primitives only.

┌─────────────────────────────────────────────┐
│  Monday, April 27                     [👤]  │  ← custom header HStack (nav bar hidden)
│  Chicken                        150 C       │
│  Workout                          0 kg       │
│  Weight                          96 kg       │
│  2 cups of water                0.5 L       │
│  Write a food...                            │  ← rotating placeholder
│                                             │
│ ┌─ SummaryCard ────────────────────────┐    │
│ │ ⭕20% 450/2226   15g  54g  18g  💧   │    │  ← .safeAreaInset(.bottom)
│ │      1776 left   p    c    f   0.2L  │    │
│ └──────────────────────────────────────┘    │
└─────────────────────────────────────────────┘

Journal input grammar

Everything the user can log gets typed into the same field. JournalViewModel.submit tries intercepts in this order:

  1. Workout keywords (workout / gym / lift / train, optional name hint after a space) → start/end an ActiveSession.
  2. Cardio keywords (run / running / jog / endurance / sprint / swim / row / bike / cycle / cardio, optional context after a space) → start a CardioBout and mount ActiveCardioCard in the same overlay slot the workout card uses.
  3. Bare weight (96 kg, 185 lb, 70.5 kilo, 96kg) → WeightLogEntry.
  4. Anything elseFoodResolving resolver → FoodLogEntry.

Four log types share the day’s feed (FeedItem.food / .workout / .weight / .cardio), sorted chronologically with swipe-to-delete and per-type detail sheets.

Visual language

Where stuff lives

WorkoutApp/
├── App/
│   ├── WorkoutAppApp.swift        ← @main entry, hosts JournalView
│   ├── AppEnvironment.swift       ← DI container (resolver picker)
│   ├── AppConfig.swift            ← reads Info.plist → Supabase + AI keys
│   └── SummaryCard.swift          ← Mist-style bottom summary
├── Design/                        ← Theme + view modifiers + atoms (CalorieRing, WaterCup, …)
├── Models/                        ← @Model SwiftData types
├── Services/                      ← FoodResolver protocol + impls + PersistenceController
├── Features/
│   ├── Journal/                   ← JournalView, ActiveWorkoutCard, food/weight/workout rows
│   ├── Insights/                  ← (legacy Overview/Nutrients/Training — slated for redesign)
│   └── Profile/                   ← Form-based settings + detail pages
└── Resources/                     ← Assets + Info.plist (managed by xcodegen) + Secrets.example.xcconfig

supabase/
├── migrations/0001_init.sql       ← schema + RLS + auth trigger
└── functions/resolve-food/        ← edge fn (next iteration)

docs/
├── BACKEND_SETUP.md               ← step-by-step infra setup
├── ARCHITECTURE.md                ← bigger-picture
├── BUILD_AND_RUN.md
├── DESIGN_SYSTEM.md
└── ROADMAP.md

scripts/                           ← seed-usda, seed-open-food-facts (next iteration)

xcodegen generate rebuilds WorkoutApp.xcodeproj from project.yml. The managed Info.plist injects SUPABASE_URL / SUPABASE_ANON_KEY / AI_PROVIDER / AI_API_KEY from the gitignored Secrets.xcconfig via $(VAR) substitution at build time.

Workout flow

Journal: user types "gym" + Done
   → JournalViewModel.submit detects keyword → sets pendingWorkoutLaunch
   → JournalView.handleWorkoutToggle creates WorkoutSession(status: .active)
   → ActiveWorkoutCard slides up via .safeAreaInset(.bottom), replaces SummaryCard
   → Templates dropdown auto-opens with "New blank workout" + saved templates

User picks a template OR "New blank workout" → exercise 1 created with EMPTY name
                                              → user taps to type the exercise name

User types reps/weight. Swipe left/right anywhere on the card body → next /
previous exercise. The `+` next to the exercise name appends a fresh blank
exercise. Subtle chevron-in-circle hints on the left and right edges of the
card make the swipe affordance discoverable.

User taps anywhere outside the card (on the dimmed background) → marks the
session completed → card slides out → row appears in the day's feed as
   `Workout` on the left + total kg/lb pushed on the right (no subtitle —
   each journal row carries a single trailing value). Tapping a completed
   row re-mounts the same `ActiveWorkoutCard` in the centered overlay
   (populated with the session's exercises/sets), *not* a sheet — so the
   review feels like a fresh log card with the data filled in.

Finish: tap anywhere on the dimmed area outside the card → marks the
session completed and saves whatever's been entered. To delete a
finished session entirely, swipe-left the journal row.

Workout plans live in Profile → Workout plans. Each plan has a name + JSON-encoded [TemplateExercise] (each with default [TemplateSet] carrying optional reps/weight). They surface in the active card’s chevron-down dropdown.

Backend

Read docs/BACKEND_SETUP.md for the full setup. Headlines:

Resolution flow

USER TYPES "150g chicken"   (or "1 cup broccoli")
        │
iOS parses quantity + name
        │
        ▼
Edge Function `resolve-food`:
  1. Search foods + food_aliases
       HIT  → scale per-100g macros to 150g (or look up `cup → grams` via
              food_servings, then scale)
       MISS → call Groq with the resolver prompt
                  ↓
              Validate JSON → INSERT foods row + aliases + servings
                  ↓
              Return resolved entry
        │
        ▼
iOS writes food_log_entries row (RLS scopes to user)

When no quantity is given ("chicken"), the resolver assumes 100g. Foods include food_servings rows so volume inputs ("1 cup broccoli") resolve via the food + cup-to-grams lookup. Failed AI / offline → entry stored as needs_review; tapping the row opens an editable detail sheet for manual macro entry.

Apple Health

Write-only. We push food / water / workout / weight logs to HealthKit so the user’s other apps see them. We never read from HealthKit. NSHealthShareUsageDescription will be removed when the HealthKit writer ships.

Planned: progression engine (workouts)

Algorithm shape (rules-based, no AI). Every exercise the user logs in a workout_sessions accumulates into per-exercise history. When the user opens a new session for the same exercise, the resolver suggests the next set:

  1. Use double progression with default 5–8 rep range (per-user configurable in Profile).
  2. Suggested next set = last session’s reps × weight, with these rules:
    • Hit top of rep range last session → +2.5 kg lower body, +1.25 kg upper body, drop to bottom of range.
    • Below top → same weight, +1 rep.
    • Failed (couldn’t hit bottom of range) 3 sessions in a row → −10 % deload.
  3. AI is only used to interpret weird user input (e.g. “I went heavier than usual”), never to make the prediction itself — too unpredictable.

This lives in Services/Progression/ (file does not exist yet). Computed on session open from the previous N sessions; user can override anytime.

Cardio (implemented)

Cardio is a different shape from strength — distance + time + effort instead of reps × weight. Lives in:

Inline shorthand parsing ("5 km swim", "30 min bike", "5x100m sprints") is not yet wired — typing a cardio keyword always opens the card; the user fills the fields.

Planned: Insights redesign

Three sections (replaces the legacy Overview/Nutrients/Training tabs):

Apple-Health-style cards. Designed; not implemented.

How to navigate by intent

If you want to… Open
Change visual tokens (colors, spacing, shadows) WorkoutApp/Design/Theme.swift — never hardcode
Change Journal layout / behavior WorkoutApp/Features/Journal/README.md
Change the workout card / template dropdown WorkoutApp/Features/Journal/ActiveWorkoutCard.swift
Change the bottom summary card WorkoutApp/App/SummaryCard.swift
Change the calorie ring or water cup WorkoutApp/Design/CalorieRing.swift + WorkoutApp/Design/WaterCup.swift
Change input grammar / placeholder rotation WorkoutApp/Features/Journal/JournalViewModel.swift + FoodInputField.swift
Change Profile fields, units, plans WorkoutApp/Features/Profile/README.md
Edit a workout plan UI WorkoutApp/Features/Profile/WorkoutPlanEditor.swift
Change food → nutrition resolution WorkoutApp/Services/FoodResolver/README.md
Wire / change the backend docs/BACKEND_SETUP.md + supabase/migrations/ + WorkoutApp/Resources/Secrets.example.xcconfig
Add a SwiftData field/entity WorkoutApp/Models/README.md
Build / run docs/BUILD_AND_RUN.md
Bigger architectural picture docs/ARCHITECTURE.md
What’s next docs/ROADMAP.md

Working rules in this repo

  1. Source of truth for visual values is Design/Theme.swift. Never hardcode.
  2. Black, white, and grayscale only. The water cup is the only colored element. Adding color requires explicit user approval.
  3. Apple-native first. No custom tab bars, no copy-of-Mist widgets — NavigationStack, Form, .toolbar, .sheet, .safeAreaInset are the building blocks.
  4. Features depend on protocols, not concretes. Side-effect protocols live in Services/.
  5. Re-run xcodegen generate after adding a file or folder. Sources are auto-discovered, but the project file needs a refresh.
  6. Per-folder READMEs are mandatory. Short — pointer-style, not tutorials.
  7. No backend / AI keys live in iOS. The Edge Function holds the Groq key; the app only ships SUPABASE_URL + SUPABASE_ANON_KEY.
  8. iOS deployment target: 26.0. Don’t lower it.
  9. No Workout / Insights / Profile tab. Workouts come from Journal keywords. Profile lives behind the toolbar icon. Insights lives behind tapping the SummaryCard.
  10. Adding optional @Model fields requires no migration; adding non-optional fields breaks dev stores. New fields → optional + accessor with fallback.
  11. WorkoutTemplate.templateExercises is the canonical source of plan content (JSON-backed). Legacy exerciseIds is only for back-compat reads.
  12. Tap-outside finishes both cards. Tapping the dimmed area outside the active workout or cardio card saves whatever’s been entered and marks the session/bout completed — same outcome as the cardio ✓. There’s no explicit “Workout finished” / “Done” button anymore. To delete a finished session entirely, swipe-left the journal row.
  13. Ask before guessing. When the user’s instructions are ambiguous, the visual target isn’t pinned down, the data shape is unclear, or there are multiple reasonable interpretations, stop and ask rather than picking one and shipping it. Auto mode is not a license to guess: it speeds up unambiguous work, not uncertain work. A quick clarifying question prevents an iteration spent undoing the wrong interpretation.

Tooling