Skip to content

wathmal/workout-sync

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

144 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Fit Sync

A personal fitness-tracking hub that pulls the whole training picture into one dashboard — strength, cardio, nutrition, body composition, and racing. It ties together several integrated functionalities:

  • Workout sync — upload a workout photo; vision extracts exercises, sets, and reps, matches them to Hevy's catalog, and pushes the result to Hevy.
  • Food / macro log — log meals via search, free text, photo, or barcode against a local Postgres-backed macro store with time-boxed targets.
  • Weekly training agenda — a 7-day plan-vs-done view that merges planned sessions (Google Calendar) with completed ones (Hevy + Garmin), with a navigable week slider to review past weeks.
  • Activity sync — Garmin runs, walks, and other cardio synced and reconciled against Hevy lifts (no double-counting).
  • Body composition — measurements card plus weight and body-fat trend over time.
  • Race calendar — track upcoming events, log results, and count down to the next race.

A single dashboard surfaces it all: weekly workout/run summary, muscle coverage, calories vs target, body-composition trend, a race countdown, and the merged weekly agenda.

Built with Next.js 16, React 19, TypeScript, Tailwind v4, shadcn/ui, Drizzle, and Postgres.

Screenshots

Dashboard

Muscle coverage, calories vs target, weekly agenda, race timeline, and body-composition trend in one view.

Dashboard

Workout sync (Hevy)

Drop a workout photo — vision parses every exercise, set, and rep, then matches Hevy's catalog.

Workout upload

Food / macro log

Log meals by search, brand lookup, text, photo, or barcode against the local macro store.

Food log

Features

Workout sync (Hevy)

  • Photo upload with EXIF date extraction (HEIC/HEIF supported)
  • Groq Vision (Llama 4 Scout) extracts exercises, sets, reps, weights
  • Optional agent harness (tool-use loop via Groq, LM Studio, or local Claude CLI)
  • Fuzzy + embedding match against Hevy's catalog (461 exercises)
  • Review and edit before sync; duplicate detection by date

Food / macro log

  • Backed by food-macro-api (FMA) for search and analysis
  • Five input modes: search, Open Food Facts brand lookup, text, photo, barcode
  • Persistent log in local Postgres (Drizzle ORM)
  • Time-boxed macro targets, week stack, quick-add chips

Dashboard

  • Muscle coverage map driven by recent Hevy workouts
  • Calorie/macro summary against active target
  • Body measurements card and trend chart
  • Weekly agenda merging planned (Google Calendar) and done (Hevy + Garmin) sessions
  • Race timeline counting down to the next event

Weekly agenda (Garmin + Google Calendar)

  • 7-day Mon–Sun view that flips per day at 21:00 local time: planned (calendar) → done (Hevy + Garmin)
  • Calendar events filtered to Move / Perform / Race / Track titles
  • Garmin activities overlapping a Hevy lift are de-duped (Hevy stays source of truth)
  • Garmin synced via a Python subprocess; calendar via a Google service-account JWT
  • Manual sync (in-app Refresh server action) + cron sync (POST /api/agenda/sync with a shared secret)
  • Garmin + calendar cached in Postgres; Hevy read live each request
  • Pure, unit-tested merge in lib/dashboard/agenda.ts (now + tz injected)

Race calendar

  • Track races by category (Hyrox, Running, Team games) with location, notes, and event targets
  • Log results (finish time + placement) after events
  • Grouped by year and status (upcoming / next / past) with a days-until countdown
  • 12-month visual timeline on the dashboard
  • Stored in Postgres (Drizzle); seeded via npm run db:seed

3D body visualisation is on the roadmap — see https://github.com/datar-psa/clad-body for the candidate model when this lands.

Tech stack

  • Framework: Next.js 16 (App Router), React 19, TypeScript
  • Styling: Tailwind v4, shadcn/ui (New York), Radix, lucide-react
  • State: React Context providers (workout, hevy, food log, measurements, agenda, race)
  • Vision: Groq API (Llama 4 Scout) or local LM Studio; optional agent harness
  • Matching: Fuzzy (Levenshtein + bonuses) blended with embeddings (LM Studio / Transformers.js)
  • Charts: Recharts (body-composition trend)
  • Persistence: Postgres 16 + Drizzle ORM (food log, macro targets, races, Garmin + calendar cache)
  • External APIs: Hevy, Groq, FMA, Open Food Facts, Garmin (via Python subprocess), Google Calendar (service account)
  • Image processing: EXIF (exifr), HEIC convert (heic-convert), zoom/pan (react-zoom-pan-pinch)
  • Containers: Multi-stage Dockerfile, standalone output, ready for TrueNAS Scale

Getting started

Prerequisites

  • Node.js 22+ (no .nvmrc yet)
  • Docker or Podman (for local Postgres + FMA)
  • A running food-macro-api instance if you want the food log
  • Python 3 + garminconnect (optional — only for the Garmin side of the weekly agenda)

Install

git clone <repository-url>
cd workout-sync
npm install
cp .env.example .env.local     # fill in keys
docker compose up -d db        # local Postgres on :5433
npm run db:migrate
npm run db:seed                # optional, seeds a starter macro target + sample races
npm run dev                    # http://localhost:3000

Without GROQ_API_KEY the workout extraction falls back to mock data and the UI shows a warning banner. Without FMA_BASE_URL/FMA_API_KEY the food routes return errors. The Garmin + Calendar vars are optional — without them the weekly agenda just shows Hevy sessions and an empty planned side.

Environment variables

See .env.example for the full template. Highlights:

Var Required Purpose
GROQ_API_KEY recommended Workout vision extraction
HEVY_API_KEY for sync Hevy API (catalog refresh + workout push)
FMA_BASE_URL for food log food-macro-api base URL
FMA_API_KEY for food log FMA bearer token
DATABASE_URL for food log Postgres connection string
USER_TZ optional IANA timezone for day-boundary aggregation + the agenda 21:00 day-switch
GARMIN_EMAIL / GARMIN_PASSWORD for agenda Garmin credentials — fetch.py auto-mints + refreshes the token (requires 2FA off; stores the password at rest)
GARMIN_TOKEN_DIR optional Container-local token cache (default /tmp/garmin-token, no volume)
GARMIN_TOKEN_B64 optional First-boot token seed, minted off-box via scripts/garmin/bootstrap.py; superseded by GARMIN_EMAIL/GARMIN_PASSWORD
GOOGLE_SA_KEY for agenda Google service-account key (inline JSON, base64, or file path)
GCAL_ID for agenda Calendar id to read (the calendar's address, not primary)
AGENDA_SYNC_SECRET for cron Shared secret for POST /api/agenda/sync; required on internet-exposed deploys
MATCHING_MODE optional fuzzy / vector / both
EMBEDDING_SOURCE optional auto / lm-studio / transformers / off
AGENT_HARNESS_PROVIDER optional off / groq / lm-studio / claude-cli

Full list with defaults: docs/architecture.md and docs/agenda-integration.md.

Scripts

npm run dev                       # Next.js dev server
npm run build                     # production build (prebuild refreshes Hevy catalog + embeddings)
npm run lint                      # ESLint
npm test                          # Jest
npm run test:watch                # Jest watch

npm run db:up                     # start local Postgres
npm run db:down                   # stop it
npm run db:generate               # drizzle-kit generate (after schema edit)
npm run db:migrate                # apply migrations
npm run db:seed                   # seed a default macro target + sample races
npm run db:reset                  # nuke volume + remigrate + reseed

npm run refresh:hevy              # refresh Hevy catalog snapshot
npm run build:embeddings          # build embedding catalogs (auto provider)
npm run build:embeddings:both     # build LM Studio + Transformers.js catalogs
npm run build:embeddings:check    # exit 0 if catalogs up-to-date

npm run e2e:matching              # matching only
npm run e2e:full                  # Groq + matching (needs GROQ_API_KEY)
npm run e2e:heic                  # HEIC + Groq

npm run debug:match -- "DB Curl"  # score breakdown for one query
npm run compare:modes             # compare matching modes side by side
npm run agent:extract -- tests/fixtures/workout-revl-1.jpeg

Container deployment

Build

podman build -t workout-sync:latest .
# Multi-arch (for TrueNAS):
podman buildx build --platform linux/amd64 -t workout-sync:amd64 .

The image bundles a Python venv for the Garmin subprocess and COPYs scripts/garmin/ into the runner (Next standalone output excludes it). GARMIN_PYTHON is set to the venv interpreter inside the image.

Run

podman run -d \
  --name workout-sync \
  -p 3000:3000 \
  -e HEVY_API_KEY=... \
  -e GROQ_API_KEY=... \
  -e DATABASE_URL=... \
  -e FMA_BASE_URL=... \
  -e FMA_API_KEY=... \
  -e USER_TZ=Australia/Brisbane \
  -e GARMIN_EMAIL=... \
  -e GARMIN_PASSWORD=... \
  -e GOOGLE_SA_KEY=... \
  -e GCAL_ID=... \
  -e AGENDA_SYNC_SECRET=... \
  workout-sync:latest

Push to Docker Hub

podman login docker.io
podman tag workout-sync:latest docker.io/<user>/workout-sync:v0.3.0
podman push docker.io/<user>/workout-sync:v0.3.0

TrueNAS Scale

Apps → Custom App → Install. Set image repo, env vars, port mapping (container 3000). For private repos configure Docker Hub credentials. To keep the agenda fresh, point a cron at POST /api/agenda/sync with the x-sync-secret header set to AGENDA_SYNC_SECRET.

See docs/architecture.md for the deployment env table and embedding/agent-harness options inside containers, and docs/agenda-integration.md for the Garmin + Calendar setup.

Architecture

Single source of truth: docs/architecture.md. Quick map:

Photo / barcode / text / search input
   ↓
/api/process-workout (Groq or agent harness)        /api/food/{search,off-search,analyze/*}
   ↓                                                  ↓
fuzzy + embedding match (Hevy catalog)              food-macro-api (FMA) + Open Food Facts
   ↓                                                  ↓
review / edit                                       review / edit
   ↓                                                  ↓
/api/hevy-sync → Hevy API                           /api/food/log → Postgres
   ↓                                                  ↓
hevy-provider (last 14d, muscle coverage)           food-log-provider (today, week, target, quick-add)
                       \                            /
                        dashboard (`app/dashboard`)
                       /                            \
agenda-provider (/api/agenda)                race-provider (/api/races)
  Hevy + Garmin + Google Calendar              Postgres race_event

Data structures

Exercise (Hevy shape):

{
  id: "uuid",
  title: "Exercise Name",
  type: "weight_reps",
  primary_muscle_group: "chest",
  secondary_muscle_groups: ["triceps"],
  is_custom: false,
  equipment: "barbell"
}

Workout:

{
  id: "uuid",
  date: Date,
  duration_minutes: number,
  caption: string,
  exercises: WorkoutExercise[]
}

Food log entry (Drizzle, see lib/food/schema.ts):

{
  id, batchId, loggedAt, source,
  name, grams,
  kcal, proteinG, carbsG, fatG,
  kcalPerG, proteinPerG, carbsPerG, fatPerG, // for local rescale on edit
  fmaFoodId, fmaSource, fmaSourceId,
  confidence, warnings, rawResponse, mealName, note
}

Race event (Drizzle, see lib/db/schema/race.ts):

{
  id, name, date, category,         // category: hyrox | running | team | ...
  eventTarget, location, note,
  resultTime, resultPlacement, resultNote,
  createdAt, updatedAt
}

License

MIT. See LICENSE.

About

All in one personal training dashboard. Log workouts with Hevy, track macros, body composition and races.

Resources

License

Stars

1 star

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors