Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 20 additions & 25 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
## Project Overview

Workout Tracker a self-hosted, mobile-first SvelteKit app for logging workouts with progressive overload tracking. Single-user, no auth. See `llm-docs/v1/REQUIREMENTS.md` for full product requirements and `llm-docs/v1/TECHNICAL_DESIGN.md` for architecture details.
Workout Tracker -- a self-hosted, mobile-first SvelteKit app for logging workouts with progressive overload tracking. Single-user, no auth. See `docs/` for architecture, database schema, offline design, and deployment. See `llm-docs/` for original product requirements and technical design.

## Tech Stack

Expand Down Expand Up @@ -50,41 +50,36 @@ Two test projects configured in `vite.config.ts`:

Any documentation written by agents must be clear, simple, and short. No unnecessary verbosity.

## Architecture
## Key File Locations

- **DB schema**: `src/lib/server/db/schema.ts` — Drizzle schema definition
- **DB connection**: `src/lib/server/db/index.ts` — creates connection with auto-detected path
- **Drizzle config**: `drizzle.config.ts` — migration config, schema path, SQLite dialect
- **Shared utils**: `src/lib/utils.ts` — `cn()` helper for Tailwind class merging + shadcn type helpers
- **Routes**: `src/routes/` — SvelteKit file-based routing
- **Components**: `src/lib/components/` — organized by domain (layout, workout, program, history, exercises, shared, ui)
- **Server logic**: `src/lib/server/` — DB queries in `db/queries/`, utilities in `utils/`
- **DB schema**: `src/lib/server/db/schema.ts`
- **DB connection**: `src/lib/server/db/index.ts`
- **DB queries**: `src/lib/server/db/queries/`
- **Drizzle config**: `drizzle.config.ts`
- **Shared utils**: `src/lib/utils.ts`
- **Routes**: `src/routes/`
- **Components**: `src/lib/components/` (layout, workout, program, shared, ui)
- **Server logic**: `src/lib/server/`
- **Offline module**: `src/lib/offline/`

## Documentation Lookup Rules

**CRITICAL: Never rely on training data for syntax or APIs. Always look up latest docs.**

- **Svelte/SvelteKit**: Use the Svelte MCP server (`list-sections` then `get-documentation`). Always run `svelte-autofixer` on any Svelte code before finalizing.
- **shadcn-svelte**: Use https://www.shadcn-svelte.com index at https://www.shadcn-svelte.com/llms.txt
- **Svelte/SvelteKit**: Use the Svelte MCP server (`list-sections` then `get-documentation`). Always run `svelte-autofixer` on any Svelte code before finalising.
- **shadcn-svelte**: Use https://www.shadcn-svelte.com -- index at https://www.shadcn-svelte.com/llms.txt
- **All other technologies**: Use the Context7 MCP Server for up-to-date documentation.

## Svelte MCP Tools

1. **list-sections** Use this FIRST to discover all available documentation sections. Returns a structured list with titles, use_cases, and paths. When asked about Svelte or SvelteKit topics, ALWAYS use this tool at the start of the chat to find relevant sections.
2. **get-documentation** Retrieves full documentation content for specific sections. Accepts single or multiple sections. After calling the list-sections tool, you MUST analyze the returned documentation sections (especially the use_cases field) and then use the get-documentation tool to fetch ALL documentation sections that are relevant for the user's task.
3. **svelte-autofixer** — Analyzes Svelte code and returns issues and suggestions. You MUST use this tool whenever writing Svelte code before sending it to the user. Keep calling it until no issues or suggestions are returned.
1. **list-sections** -- Use this FIRST to discover all available documentation sections.
2. **get-documentation** -- Retrieves full documentation content for specific sections. After calling list-sections, analyse the returned sections and fetch ALL relevant ones.
3. **svelte-autofixer** -- Analyses Svelte code and returns issues and suggestions. MUST be used on any Svelte code before finalising. Keep calling until no issues remain.

## Context7 MCP Tools

1. **resolve-library-id** — Resolves a general library name into a Context7-compatible library ID.

- `query` (required): The user's question or task (used to rank results by relevance)
- `libraryName` (required): The name of the library to search for

2. **query-docs** — Retrieves documentation for a library using a Context7-compatible library ID.

- `libraryId` (required): Exact Context7-compatible library ID (e.g., shadcn-svelte.com/docs, tailwindcss.com/docs)
- `query` (required): The question or task to get relevant documentation for
1. **resolve-library-id** -- Resolves a library name into a Context7-compatible library ID.
2. **query-docs** -- Retrieves documentation for a library using a Context7-compatible library ID.

## Branching & PR Workflow

Expand Down Expand Up @@ -121,7 +116,7 @@ The `playwright-cli` skill is available for any Playwright browser automation ac

At the end of each phase, a verifier subagent MUST be kicked off to check:

1. Everything is built as per the plan no outstanding items
1. Everything is built as per the plan -- no outstanding items
2. No regressions or bugs introduced
3. All rules in this file were followed no shortcuts taken
3. All rules in this file were followed -- no shortcuts taken
4. Code quality: DRY/YAGNI, clear, self-documenting
45 changes: 12 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
# Workout Tracker

A self-hosted, mobile-first workout logging app with progressive overload tracking. Built with SvelteKit, SQLite, and Docker.
A self-hosted, mobile-first workout logging app built with SvelteKit and SQLite. Create workout programmes, log sets with weight and reps, and track progressive overload over time. Single-user, zero-config, works offline as a PWA.

## Requirements

- Node.js 22+
- Node.js 22+ (for local development)
- Docker (for deployment)

## Development
## Local Development

```sh
npm install
Expand All @@ -15,43 +16,21 @@ npm run dev

The database is automatically created at `./data/workout-tracker.db`.

## Deployment with Docker

### Using Docker Compose (recommended)
## Docker Deployment

```sh
docker compose up -d
```

This starts the container on port 6789 and persists the database via a volume mount to `/data`.

### Using Docker directly

```sh
docker build -t workout-tracker .
docker run -d \
-p 3000:3000 \
-v workout-data:/data \
workout-tracker
```

### Using the pre-built image from GHCR
Or use the pre-built image:

```sh
docker run -d \
-p 3000:3000 \
-v workout-data:/data \
ghcr.io/kavith-k/workout-tracker:latest
docker run -d -p 3000:3000 -v workout-data:/data ghcr.io/kavith-k/workout-tracker:latest
```

### Data persistence

The database is stored at `/data/workout-tracker.db` inside the container. Mount a volume to `/data` to persist data across container restarts.

### Configuration

No environment variables are required. The app is zero-config.

### HTTPS
## Documentation

Service workers and offline features require HTTPS in most browsers. This works automatically on `localhost`, but remote access over plain HTTP will disable offline support. Use a reverse proxy (e.g., Caddy, Traefik, nginx) to terminate TLS if accessing the app over the network.
- [Architecture](docs/architecture.md) -- tech stack, directory structure, data flow
- [Database](docs/database.md) -- schema, relationships, migration workflow
- [Offline](docs/offline.md) -- PWA, IndexedDB queue, sync engine
- [Deployment](docs/deployment.md) -- Docker, reverse proxy, HTTPS, data persistence
122 changes: 122 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# Architecture

## Tech Stack

| Layer | Technology |
| --------- | ------------------------------------- |
| Framework | SvelteKit 2 (Svelte 5), Node adapter |
| Styling | TailwindCSS v4 + shadcn-svelte |
| Database | SQLite via better-sqlite3 + Drizzle ORM |
| Offline | PWA with Service Worker (Workbox) |
| Testing | Vitest (unit/component) + Playwright (E2E) |

## Directory Structure

```
src/
lib/
server/
db/
index.ts # DB connection (auto-detected path)
schema.ts # Drizzle schema definition
queries/ # Reusable query functions
programs.ts # Programme CRUD
exercises.ts # Exercise CRUD
workouts.ts # Workout session lifecycle
history.ts # History browsing and deletion
export.ts # JSON/CSV export assembly
utils/ # Server-side utilities
components/
ui/ # shadcn-svelte components
layout/ # App shell, navigation, resume banner
home/ # ConsistencyGrid (GitHub-style activity heatmap)
program/ # Programme form (drag-to-reorder via svelte-dnd-action)
workout/ # Workout wizard, ExerciseStep (set logging, copy-down, volume)
shared/ # Offline indicator, empty state, confirm dialog
offline/
queue.ts # IndexedDB queue management
sync.ts # Sync engine (periodic + on-reconnect)
stores.svelte.ts # Reactive online/sync status
utils.ts # cn() helper, shadcn type helpers
routes/ # SvelteKit file-based routing (see below)
app.html
app.css # Tailwind imports, global styles
static/ # PWA icons, favicon
drizzle/ # Generated migrations
e2e/ # Playwright E2E tests
scripts/
seed-test-data.mjs # Wipe DB and populate with 2 years of sample data
```

## Routes

| Route | Purpose |
| ------------------------------ | --------------------------------------------- |
| `/` | Home screen with active programme days and consistency grid |
| `/workout/start` | Form action to create session and redirect |
| `/workout/[sessionId]` | Active workout logging interface |
| `/workout/[sessionId]/summary` | Post-workout summary with PRs and stats |
| `/history` | Past workouts by date with infinite scroll |
| `/history/[sessionId]` | Single workout session details |
| `/api/history` | GET endpoint for paginated history (infinite scroll) |
| `/programs` | List and manage programmes |
| `/programs/new` | Create new programme |
| `/programs/[programId]` | Edit existing programme |
| `/exercises` | Exercise library with stats |
| `/settings` | Data export (JSON/CSV) |
| `/api/sync` | POST endpoint for offline queue synchronisation |

## Data Flow

### Starting a Workout

1. User taps a workout day on the home screen
2. `POST /workout/start` creates a `workout_session` (status: `in_progress`), pre-creates `exercise_logs` and empty `set_logs`
3. Redirects to `/workout/[sessionId]`
4. Load function fetches session details, exercise logs with sets, and progressive overload data

### Logging Sets

Each set row is a `<form action="?/updateSet">` with `use:enhance`. Weight and reps inputs fire `requestSubmit()` on change for immediate server-side persistence. The enhance callback skips `update()` to avoid re-rendering during active editing.

If the server is unreachable, the action is queued to IndexedDB for later sync (see [offline docs](offline.md)).

A **copy-down** button (arrow icon) appears on empty sets, copying weight and reps from the previous set. Live **volume** (weight x reps summed) is shown per exercise alongside the previous session's volume.

### Completing a Workout

1. User taps "Stop" and confirms
2. Unlogged exercises are auto-marked as `skipped`
3. If **no exercises** have any logged reps, the workout is cancelled: all logs and the session are deleted, and the user is redirected home with a cancellation message
4. Otherwise, session marked as `completed` and redirected to summary page with PRs and stats

### Programme Editing

Days and exercises within the programme form are reordered via **drag-and-drop** (using `svelte-dnd-action` with drag handles). Touch-friendly with flip animations.

### Stale Workout Cleanup

The root layout server load calls `closeStaleWorkouts()` on every page load. Any `in_progress` session older than 4 hours is auto-completed with unlogged exercises marked as `skipped`. This prevents abandoned workouts from blocking new ones.

## Test Data

To populate the database with realistic sample data for development or testing:

```bash
node scripts/seed-test-data.mjs
```

This deletes the existing database, runs migrations to recreate the schema, then seeds ~240 completed workout sessions spanning 2 years (Feb 2024 -- Feb 2026) with progressive overload, sporadic scheduling, and occasional skipped exercises.

## Key Design Decisions

| Decision | Rationale |
| --------------------------- | ------------------------------------------------------------- |
| Drizzle ORM | Type-safe, lightweight, good SQLite support |
| SvelteKit form actions | Progressive enhancement, works with PWA offline fallback |
| Snapshot fields in logs | Preserves historical accuracy when entities are renamed/deleted |
| Per-set immediate save | Maximum data safety, survives crashes/closes |
| Single active programme | Simplifies home screen and workout start flow |
| IndexedDB for offline queue | Browser-native, reliable, works with service workers |
| svelte-dnd-action | Touch-friendly drag-and-drop with Svelte integration |
| Empty workout cancellation | Prevents clutter from accidental/empty workout sessions |
111 changes: 111 additions & 0 deletions docs/database.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# Database

SQLite database managed by Drizzle ORM. Schema defined in `src/lib/server/db/schema.ts`.

## Connection

The database path is auto-detected:

- **Local dev**: `./data/workout-tracker.db` (created automatically)
- **Docker**: `/data/workout-tracker.db` (detected by the presence of `/data/`)

No environment variables required.

## Schema

```
programs
id INTEGER PK autoincrement
name TEXT NOT NULL
is_active BOOLEAN NOT NULL DEFAULT false
created_at TIMESTAMP NOT NULL
updated_at TIMESTAMP NOT NULL

workout_days
id INTEGER PK autoincrement
program_id INTEGER FK -> programs(id) ON DELETE CASCADE
name TEXT NOT NULL
sort_order INTEGER NOT NULL
created_at TIMESTAMP NOT NULL

exercises
id INTEGER PK autoincrement
name TEXT NOT NULL UNIQUE
unit_preference TEXT NOT NULL DEFAULT 'kg' ('kg' | 'lbs')
created_at TIMESTAMP NOT NULL

day_exercises
id INTEGER PK autoincrement
workout_day_id INTEGER FK -> workout_days(id) ON DELETE CASCADE
exercise_id INTEGER FK -> exercises(id)
sets_count INTEGER NOT NULL DEFAULT 3
sort_order INTEGER NOT NULL
created_at TIMESTAMP NOT NULL

workout_sessions
id INTEGER PK autoincrement
program_id INTEGER FK -> programs(id) ON DELETE SET NULL (nullable)
workout_day_id INTEGER FK -> workout_days(id) ON DELETE SET NULL (nullable)
program_name TEXT NOT NULL (snapshot)
day_name TEXT NOT NULL (snapshot)
status TEXT NOT NULL DEFAULT 'in_progress' ('in_progress' | 'completed')
started_at TIMESTAMP NOT NULL
completed_at TIMESTAMP (nullable)

exercise_logs
id INTEGER PK autoincrement
exercise_id INTEGER FK -> exercises(id) ON DELETE SET NULL (nullable)
session_id INTEGER FK -> workout_sessions(id) ON DELETE CASCADE
exercise_name TEXT NOT NULL (snapshot)
status TEXT NOT NULL DEFAULT 'logged' ('logged' | 'skipped')
is_adhoc BOOLEAN NOT NULL DEFAULT false
sort_order INTEGER NOT NULL
created_at TIMESTAMP NOT NULL

set_logs
id INTEGER PK autoincrement
exercise_log_id INTEGER FK -> exercise_logs(id) ON DELETE CASCADE
set_number INTEGER NOT NULL
weight REAL (nullable)
reps INTEGER (nullable)
unit TEXT NOT NULL DEFAULT 'kg' ('kg' | 'lbs')
created_at TIMESTAMP NOT NULL
```

## Relationships

```
Program 1---* WorkoutDay 1---* DayExercise *---1 Exercise
|
WorkoutSession 1---* ExerciseLog 1---* SetLog |
| | |
*---1 Program? *---1 Exercise? |
*---1 WorkoutDay? (snapshot: exercise_name) |
(snapshots: program_name, day_name)
```

## Key Schema Decisions

- **Snapshot fields**: `program_name`, `day_name`, `exercise_name` are stored at workout time. Historical logs remain accurate even if the source is renamed or deleted.
- **Nullable FKs**: `program_id`, `workout_day_id`, `exercise_id` become null if the referenced entity is deleted, but snapshot fields preserve the data.
- **Sort order**: `workout_days` and `day_exercises` have `sort_order` for user-defined ordering.
- **Unit preference**: Stored per-exercise in `exercises` and per-set in `set_logs`.
- **Status tracking**: Sessions track `in_progress` vs `completed`. Exercise logs track `logged` vs `skipped`.

## Migration Workflow

```bash
# Generate a migration after changing schema.ts
npm run db:generate

# Run pending migrations
npm run db:migrate

# Push schema directly (dev only, no migration file)
npm run db:push

# Open Drizzle Studio for visual inspection
npm run db:studio
```

Migrations are stored in `drizzle/` and are copied into the Docker image at build time.
Loading