Skip to content

About

No description, website, or topics provided.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Latest commit

 

History

59 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Training Board

A gym scoreboard over 1,324 animated exercises. Pick a split, pick a muscle, decide whether you have weights, then log sets against the exercise while the animation plays.

The board: today's totals over a filterable exercise grid

Run it

You need Node 20 or later with pnpm, and an Appwrite instance to point at: either Appwrite Cloud or a self-hosted one. There is no local database, so the app cannot start without one.

pnpm install
cp .env.example .env              # fill in the Appwrite block, see DEPLOY.md
pnpm setup:appwrite               # create the database, tables, indexes and bucket
node scripts/build-dataset.mjs    # slim the sibling dataset, copy media into public/
pnpm seed:user                    # the admin account, from .env
pnpm seed                         # 2,648 media files and 1,324 exercises
pnpm dev                          # http://localhost:3000

DEPLOY.md is the long version, and worth reading the first time: creating the Appwrite project, which API key scopes it needs, what to do if a step fails, and putting the whole thing on a VPS behind a reverse proxy.

To use it on your phone in the gym, hit the Network URL that pnpm dev prints (same wifi), or pnpm build && pnpm start.

The schema

src/seed/schema.ts is the schema. It is the only written-down copy of the tables, columns and indexes, so read it rather than clicking through the console when you want to know the shape of something. pnpm setup:appwrite applies it.

It is idempotent: it creates what is missing, reports what is already there, and changes nothing else. There are no migrations and nothing runs at boot, so adding a column means adding it to schema.ts, re-running the script, and updating the row mappers in src/app/(frontend)/lib.ts in the same commit.

Seeing the data

There is no admin panel in this app. Rows, files and accounts live in Appwrite, so the Appwrite console for your project is where you read and edit them. It is a separate application behind its own login, and nothing here links to it on purpose.

  • Rows: Databases → training → the table
  • Animations and thumbnails: Storage → media
  • Accounts, and the role in an account's prefs: Auth

From inside the app, /progress is your own history and /people is the admin overview of everyone's training.

Screens

Board (/) — today's volume, sets, reps and per-split bars across the top, filters underneath, exercise grid below. A red count on a card means you already worked it today.

Exercise (/exercise/[id]) — animation, numbered steps, muscles worked, and the set logger. It pre-fills weight and reps from your last set of that exercise, shows today's sets, your best set ever, and what you did last session. Below that, other exercises for the same muscle, for when the machine is taken.

Progress (/progress) — sessions, streaks, weekly and all-time volume, heaviest set per exercise.

Editing the underlying rows, swapping an animation or managing accounts happens in the Appwrite console. See Seeing the data.

Sign in (/login) — everything above is behind it. Signed-out visitors get redirected here.

Access

Every page except /login sits in a (board) route group whose layout calls requireUser(), so a signed-out visitor is redirected. The login form posts to a server action that creates an Appwrite session and puts its secret in a first-party HttpOnly cookie; Appwrite's own cookie is bound to the Appwrite domain, which is a different host.

Reads run through an Appwrite API key, which bypasses row permissions entirely, so every query against private data constrains owner itself. The catalogue and its media are public: the exercises table and the media bucket both allow anonymous reads, because they are dataset content rather than anyone's training. tests/int/ownership.int.spec.ts fails the build if a query touching a private table is written without an owner constraint.

.env is the source of truth for the login. Change SEED_USER_PASSWORD and run pnpm seed:user: it resets the password to match and clears any lockout from failed attempts.

Filters

Filter What it does
Search Matches exercise name or equipment
Split push, pull, legs, core, cardio — derived from the target muscle
Equipment Weights (barbell, dumbbell, cable, machines) vs bodyweight (body weight, bands, balls)
Muscle All 50 muscles in the database, split into ones trained directly and ones worked as support

The muscle filter matches secondary movers too, so filtering on triceps surfaces bench press alongside the dedicated triceps work. Every filter lives in the URL, so opening an exercise and pressing back returns you to the same view.

Seeding

src/data/exercises.json is the seed input, generated from ../exercises-dataset by scripts/build-dataset.mjs. The seeder reads it and writes into Appwrite, which is the source of truth from then on. Editing an exercise in the console changes what the board shows; editing the JSON does nothing until you reseed.

pnpm setup:appwrite                            # the schema. Run this first, once
pnpm seed                                      # everything: 2,648 files, 1,324 rows
pnpm seed:rows                                 # just the rows, seconds
pnpm seed:media                                # just the files, minutes
pnpm seed:user                                 # create or repair the admin from .env
pnpm add-user                                  # MEMBER_EMAIL=sam@example.com MEMBER_NAME=Sam

Re-running is safe and resumable, which matters because the media upload is the slow part and a dropped connection halfway through should cost the remaining files rather than all of them. Exercises are matched on exerciseId, media on the id in the filename, the user on email. Options are env vars rather than CLI flags (SEED_CONCURRENCY=8), with a pnpm script for the common ones.

Programmes

A programme is a saved multi-week plan. With one active, the board shows that day's session as a checklist instead of the catalogue, and the catalogue stays one tap away behind "Browse all exercises".

Days are a list that repeats, so a four day split is four entries and not twenty-eight. Rest days are entries with rest ticked and no exercises, which is what makes them render as rows rather than as gaps in the plan.

Programmes are written in the Appwrite console, in the programs table. days is a JSON column holding the whole nested structure, because Appwrite columns are flat and two child tables would buy ordering and referential integrity the app does not use. Items reference the catalogue's four digit exerciseId, and the app hydrates the full exercise on read.

Which day is today is the calendar rule: days elapsed since startedOn, modulo the cycle length. Miss a session and the programme moves on without you, so the day picker on the board lets you pick the one you meant. That is deliberate: no rule can tell whether a missed session was a rest day, a deload or a busy Tuesday.

targetWeight is a suggestion that seeds the logger and is never applied on its own. Auto progression is the feature every lifting app adds and every serious lifter turns off, because it does not know whether the last session was hard.

How it's wired

Piece Where
Exercise catalogue exercises collection, with gif/image uploads to media
Logged sets sets collection, one row per set
Programmes programs collection, days as a repeating array
Bodyweight and scans weights and body-scans collections
Seeder src/seed/
Auth gate src/app/(frontend)/auth.ts and the (board) layout
Queries src/app/(frontend)/lib.ts
Pure scoring logic src/app/(frontend)/data.ts
Design tokens src/app/(frontend)/tailwind.css, shadcn preset b3eywfGajw
Generated media public/media/{gif,img} and media/, both gitignored

Sets are stamped with the local calendar day rather than UTC, so a late session lands on the day you trained rather than the next one. The storage bucket has image transformations disabled: Appwrite's preview endpoint returns a still frame for an animated GIF, and the animation is the point.

Rows, files and accounts all live in Appwrite, so the app container holds no state and there is nothing here to back up. DEPLOY.md covers the VPS setup: Docker Compose, the environment, and the ports.

Design

Follows DESIGN.md: a single red accent (#e60000), ink and canvas bands alternating for depth, 60px pill CTAs, 6px card chrome, no drop shadows. Outfit stands in for the proprietary display sans. The unfiltered 1,324-card grid uses content-visibility so offscreen cards skip layout, rather than truncating results or virtualising.

Plan

assets/plan/ is the build plan: fifteen files, one per feature, each a todo list with data model changes and acceptance criteria. It covers turning this into a small multi-user platform where friends see only their own training and you can see everyone's, the five gamification mechanics the research recommends, and the UI pieces that are currently missing.

Start with assets/plan/README.md. The data-ownership task it opens with is done: every query scopes to an owner, and a test enforces it.

Research

assets/research/ holds the gamification research: what the evidence says about streaks and badges for training apps, teardowns of what Tonal, Nike Run Club, Apple Fitness and others ship, the screen patterns mapped onto this design system, a phased build plan, and a setup guide for an MCP server so Claude can read and write training data.

Two findings shape the plan. A daily streak is the wrong mechanic for lifting, because rest is part of the programme, so the streak has to count weeks. And gamification features stop helping past a point, so the plan totals five mechanics and stops.

Tests

pnpm test:int      # dataset integrity, exercise queries, scoring and streaks
pnpm test:e2e      # board, filter persistence, exercise page

pnpm lint fails on a missing @eslint/eslintrc inherited from the original template.

About

No description, website, or topics provided.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages