Releases: DuarteSantos8/openGym
Release list
v1.2.2
Changelog
v1.2.2 — 2026-07-25
Training that moves on its own: an exercise can now be logged by time instead of reps, the
next weight follows a progression rule you choose rather than a single hard-coded hint, and
every lift carries an estimated 1RM. Plus a standalone mobile app, a shareable plan, and an
importer for your history from other apps.
Timed sets and a timer for the set itself (#16)
- ⏱️ Reps or time, per exercise. Planks, hangs, wall sits, dead hangs and loaded
carries no longer have to be filed under cardio to be timed. Each exercise in a routine
picks its own mode, and a timed set can still carry weight for a weighted plank or a
farmer's walk. ▶️ A work timer, separate from the rest timer. Start a timed set and it counts the
hold down, beeping and buzzing at zero exactly as the rest timer does, then checks the
set off itself. The two timers can never run at once — they mean opposite things.- Finishing a hold early logs the time you actually held, not the target. A 38-second
hold against a 45-second target is recorded as 38 seconds. - The mode travels everywhere it should: routine editor, workout, history, exercise
statistics (timed exercises chart their longest hold), the printable plan and the shared
plan file. - Plans made before this release are read exactly as they always were — nothing to migrate.
Progression rules you can read (#17)
- 📈 Pick a rule per routine, override it per exercise. Linear progression, Greyskull
LP (two straight sets plus an AMRAP final set, with double jumps and a 10 % reset),
double progression through a rep range, or adding time for timed work. Or none at all. - 🧾 Every target explains itself. "Every rep last time — 2.5 kg more." "Missed reps
3 sessions running — reset to 55 kg and work back up." The rule is visible before you
train, not after. - The session opens with the right weights already in the rows, instead of suggesting them
once you are standing at the bar. - 🚫 A bad session can't look like a good one. Short reps count as a miss even when you
checked the set off; a set you never checked counts as a miss because you did not do it.
Nothing advances the load on a session that fell apart. - Stalls and deloads are worked out from your log every time they are needed. Nothing is
written back into a finished workout and no counters are stored, so fixing a mistyped set
immediately produces the right next target. - Lower-body lifts step up in larger jumps than upper-body ones by default, and any
exercise can set its own step. - Bodyweight exercises progress in reps, because there is no load to add to a push-up
and no load to take off it either.
Estimated 1RM (#18)
- 💪 An estimated one-rep max for every lift, in the exercise progress card (with its
own curve you can switch to) and in the exercise detail sheet. - It always names the set it came from — "from 90 kg × 5 on 15 Jul" — because an estimate
off a heavy triple and one off a set of ten are very different claims. - 🧮 A calculator for a set you have not done yet, so the number is reachable before
there is any history. - Epley by default, and it refuses to guess above 12 reps, where the common formulas
disagree by double digits. - A new best estimate is reported at the end of a workout separately from a weight PR —
same weight for more reps is real progress, but it is not a heavier lift.
Share a plan
- 📤 Send someone your plan. Plan → Share your plan writes a small file with your
routines, the week schedule and any custom exercises they use — and nothing else. No
workouts, no weigh-ins, no settings. - Importing merges: shared routines arrive as new ones with fresh ids, custom exercises
are matched by name so they are not duplicated, and your own plan is never overwritten.
Taking the week schedule with it is optional. - 🖨️ A printable plan (Save as PDF) laid out so a single exercise never breaks across
a page.
Fixes
- A shared plan file naming an exercise this build doesn't have can no longer take the app
down. Unknown ids are dropped on import, anything that slips through renders as a
placeholder you can delete, and an error boundary around the screens means a bad state is
recoverable by switching tabs instead of reloading. - Importing from another app converts weights per row, not per file. FitNotes writes the
unit on each set, so a mixed export used to land 185 lb as 185 kg. - Numbers follow the UI language instead of a hardcoded locale, which was putting Swiss
apostrophes ("7'535 kg") in front of everyone. Volume stays in your own unit rather than
switching to tonnes, which was wrong for pound profiles. - Taking over a week schedule from a shared plan now really replaces Monday–Sunday instead
of only the days the shared file happened to fill. - The body-weight slider's ceiling follows your unit (300 kg / 660 lb).
- "Best: 85 Kg" is capitalised correctly again.
One codebase, two flavors
openGym is also a standalone mobile app — and it ships as a direct APK download, not
through app stores.
-
📱 Standalone mobile app. The same frontend now also builds as a native iPhone /
Android app (Capacitor) — the install-and-done flavor of openGym: no account, no server,
no sync. Everything stays on the phone.- State is mirrored into a file in the app's private storage on every change, so your
log survives even when the OS evicts WebView storage (iOS does). - The workout-day reminder becomes a native notification scheduled on the weekdays
your plan actually has a routine — no push server involved. - Backups go out through the OS share sheet (Files, AirDrop, mail…).
- Exercise images/animations load from the same CDN as the live demo.
npm run build:mobile, then openandroid/in Android Studio orios/in Xcode —
see docs/MOBILE.md.NOTICE.mdnow carries an AGPL §7 app-store exception.
- State is mirrored into a file in the app's private storage on every change, so your
-
🤖 Android APK, no Play Store. The official build is a signed, sideloadable APK
(~4.5 MB) from opengym.duarte-santos.ch — deliberately
store-free. docs/MOBILE.md covers building and signing your own. -
🍎 iOS reality check. Apple permits no installs outside the App Store, so there is no
iOS download; the docs explain the free options (self-hosted PWA on the home screen, or
running the native app onto your own iPhone from Xcode). -
📥 Import your history from another app. Settings → Data → Import from another app
reads an export from FitNotes (both the Android and the FitNotes 2 iOS format),
Strong and Hevy, and pulls body-weight history out of an Apple Health export.
Anything else with a date, an exercise name and weight/reps columns is read too.- Every row becomes a set, grouped into workouts by date, so your history arrives with
its real dates rather than as one lump. Hevy and Strong also carry session length, so
the activity heatmap fills in properly. - Exercise names are matched against the 1,324-exercise library — parenthetical
qualifiers like "(Barbell)" and shorthand like BB/DB are normalised, and a curated
table covers the plain names people actually log ("Bench Press", "Squat", "RDL").
Where a name is genuinely ambiguous it is not guessed at: it becomes one of your own
exercises instead, because filing years of training under the wrong lift is worse than
an unmatched name you can see and fix. - A summary shows what will happen — workouts, sets, how many exercises matched, which
ones didn't, and whether weights need converting — before anything is written. - Importing is idempotent: days you already have data for are left alone, so running it
twice, or importing from two apps, never duplicates a workout.
- Every row becomes a set, grouped into workouts by date, so your history arrives with
v1.2.1
Changelog
v1.2.1 — 2026-07-23
A muscle map across the app, and a live demo you can try without installing anything.
- 💪 Muscle map. Three places now show which muscles your training actually reaches, drawn on a
front-and-back body diagram shaded like the activity heatmap — more accent means more work.- Stats → Muscle balance aggregates a week, 30 days, 90 days or everything, lists your
hardest-worked muscles with their set counts, and names the ones that got nothing in that
period. That last list is the point of the card: the gaps are what you'd otherwise never notice.
Tap any muscle to read its name and volume. - Routine editor previews what a session hits as you build it, so a hole in the plan shows up
before you train around it for a month. - The finish screen shows what you just trained.
- Load is counted in effective sets — a set counts fully for the exercise's target muscle and
partially for its supporting ones — not in kilograms, because 100 kg of leg press and 12 kg of
lateral raise say nothing about which muscle worked harder. Shading is relative within the
period you're looking at, so the map always reads as a balance rather than an absolute. - Settings → Appearance → Body diagram switches between a male and female figure.
- The exercise dataset spells muscles inconsistently ("delts", "deltoids" and "shoulders" are one
muscle); all 50 spellings it uses are normalised onto the 18 the diagram can draw. Custom
exercises, which only carry a body part, fall back to it. The geometry is ~90 kB and loads on
demand, so the initial bundle is unchanged.
- Stats → Muscle balance aggregates a week, 30 days, 90 days or everything, lists your
- 🐛 Fixed: finishing a workout from its last exercise could blank the whole app. The
per-exercise weight sheet read the running workout without checking it was still there, and
finishing clears it while that sheet is still on screen. ▶️ Live demo at duartesantos8.github.io/openGym —
a browser-only build (VITE_DEMO=1) published to GitHub Pages on every push tomain. It boots
into guest mode with a seeded example profile (12 weeks of Push/Pull/Legs, weigh-ins, PRs) so
every screen has something to show, and it never talks to a server. Passkeys, sync and the admin
dashboard stay exclusive to self-hosted instances, which is where the backend lives.- 🖼️ Builds can point the exercise media elsewhere via
VITE_IMG_BASE/VITE_GIF_BASE— the demo
serves the ~140 MB dataset from a CDN instead of shipping it. The default (img/andgif/next
to the app) is unchanged.
v1.2.0 - redesign
Changelog
v1.2.0 — 2026-07-23
A complete visual redesign. Same app, same data — every screen redrawn.
A designed interface, not an assembled one
- 🎨 Rebuilt design system. One type scale carrying hierarchy through size instead of making
everything bold, a neutral surface ramp instead of saturated blue-greys, hairline separators
instead of outlined boxes, and motion that acknowledges a press rather than animating for
decoration. Light and dark are both first-class, and the eight accent colours now pick their
label colour by measured contrast — the default green in light mode was failing WCAG AA on
every primary button before. - ✏️ A hand-drawn icon set (77 icons, single stroke weight, drawn on one 24×24 grid) replaces
every emoji in the interface. Emoji render differently on each platform, sit on their own
baseline and can't take a theme colour, which is what made the old UI feel stitched together.
Icons inherit the surrounding text colour and optical size. - 🏋️ Routine icons. Picking an icon for a routine now offers a grouped set — strength,
equipment, cardio, recovery — instead of an emoji keyboard. Routines you already made keep
their look: the old emoji are mapped forward automatically, so nothing to migrate and nothing
to redo. ▶️ New tab bar with a raised Start button that turns into a pulsing orange Resume while a
workout is running.- 🏠 Home reads as a plan for today — week strip, today's session as one tappable row, body
weight, and your streak.
Charts
- 📈 Axis labels, gridlines and the target-weight line are visible again in dark mode. They
were painted with colour variables that no longer existed, which silently fell back to black
on black — and to no stroke at all for the lines. - 💬 The hover readout stays on screen. It used to be positioned with a fixed offset that
assumed one label width, so the first and last point pushed it under the chart's clip; it's now
placed from its measured size and kept inside the frame, dropping below the point when the
point sits high enough that the label would cover the value it reports. - 🖱️ It also goes away again — moving off the chart now clears the readout, crosshair and
marker, which previously stayed until you hovered somewhere else.
v1.1.3
Changelog
v1.1.3 — 2026-07-22
Admin dashboard for self-hosters (opt-in — off by default), equipment filtering, and
workout-screen fixes.
Admin dashboard
- 🛠️ Admin dashboard (Settings → Admin dashboard) for whoever runs the instance: a users
overview with workout counts and last-active times, plus a per-user drill-down into their full
workout history and body-weight log. - 🟢 Live "training now" — see who's mid-workout in real time, with their current exercise and
set progress, updated by a lightweight heartbeat while a workout is on screen. - 🚫 Disable / enable accounts — a disabled account is signed out and locked out everywhere
until you re-enable it. - 🔑 Invite-only signup (optional) — require an invite code to create a profile; generate and
revoke codes from the dashboard. Existing accounts are unaffected. - ⚙️ Configured via environment:
ADMIN_UIDS(comma-separated user ids who are admins) and
INVITE_ONLY=1; both default off, so a fresh instance stays open with no admin. See
.env.example. Admin access is gated by your passkey and enforced server-side.
Exercises & workout
- 🏋️ Filter exercises by equipment (#6). A second filter row under the body parts lets you
narrow the list to what you actually have — body weight, dumbbell, barbell, cable, band, and so
on — in both the Exercises library and the exercise picker. The options adapt to what you've
already selected and are ordered by how many exercises use them, so every combination on screen
has results behind it and the row stays short. Building a bodyweight-only plan is now two taps
per body part. - 🔎 Minimize the exercise animation during a workout (#12). A ⤡ Minimize / ⤢ Expand button
on the animation shrinks it to a thin strip so the set rows sit right under your thumb — no more
scrolling past a big GIF to tick off a set. Your choice is remembered and applied to every
exercise and future workout until you change it, so you set it once. Tapping the animation still
pauses/plays it as before. - ⏱️ Fixed: the rest timer froze at 0:01 (#14) instead of counting down to the end. It also
meant the timer could only be cleared with Skip, and a redundant "rest over" push notification
could still fire.
v1.1.2
Changelog
v1.1.2 — 2026-07-22
Custom exercises, full localization, and input fixes.
Custom exercises (#11)
- ✨ Create your own exercise from the exercise picker or the Exercises tab: a name and a
body part is all it takes. Your search text is pre-filled as the name, so "no match" flows
straight into "create it". - 📝 Optional description — setup, cues, anything you want to remember. It shows on the
exercise's detail and config sheets (where a built-in exercise would show its animation),
and it's searchable, so you can find your own exercises by their cues too. - 🏋️ Custom exercises behave like built-in ones everywhere — routines, supersets, workout
logging, weight suggestions, PRs, stats and history. The animation stays blank by design. - 🏃 Pick the cardio body part and it logs time + speed instead of weight × reps, like the
built-in cardio exercises. - ✏️ Edit (rename, change body part or description) or delete your custom exercises — from
their detail sheet in the Exercises tab, or straight from the exercise inside a routine via
"Edit or delete this exercise". Deleting removes them from your routines; already-logged
workouts keep their sets and still show the exercise name. (The routine sheet's old "Remove
exercise" button is now labelled "Remove from routine", so the two are no longer confusable.)
Localization (#7)
- 🌍 12 UI languages: English, Deutsch, Español, Français, Italiano, Português, Polski,
Türkçe, Русский, 中文, 한국어, हिन्दी. Pick yours under Settings → Appearance → Language;
the choice syncs with your profile like the theme does. - 📖 Localized exercise instructions for 10 of those languages (all except German and
Portuguese, which the upstream dataset doesn't cover yet — those fall back to English),
covering all 1,324 exercises. Body-part filters, equipment and muscle tags are translated
too; exercise names stay English (upstream limitation). Custom exercises are translated too. - 📅 Dates, weekday and month labels follow the selected language.
- ⚡ Zero cost when unused: the app still ships English-only by default. Each UI language is a
~7 kB chunk and each instruction pack ~80–120 kB (gzipped), downloaded only when you switch —
the initial bundle size is unchanged. - 🛠️ New
scripts/build-instructions.mjsregenerates the instruction packs from the upstream
dataset; translations live infrontend/src/locales/(PRs welcome — it's one flat
English-string → translation map per language). - Known gaps: push notification texts (sent by the server) and plural forms in some languages
are approximated; happy to take corrections from native speakers.
Fixes
- ⌨️ Weight and other numeric fields now accept a comma as decimal separator ("33,5") — iOS
decimal keyboards in many locales only offer a comma, which previously reset the field to 0.
Partial input like "33," no longer snaps to 0 while typing. (#13) - 📱 Fixed the exercise-config sheet (Sets / Reps / Weight, and the cardio variant) overflowing the
screen edge on narrow phones — the Weight stepper was clipped and could make the whole page pan
sideways in iOS Safari. Steppers now shrink to fit the viewport. (#10) - 🛡️ Added a global horizontal-overflow guard so a single too-wide element can no longer knock the
page layout off-scale.
v1.1.1
Changelog
v1.1.1 — 2026-07-21
Reliability fixes for the push notifications shipped in v1.1.0, found through live testing:
- 🌍 Workout day reminder now fires by each user's own browser-detected timezone instead of a
single server-wide one — works correctly regardless of where the server runs, and follows you
automatically if you travel. - 💾 Settings changes (like the reminder time) are flushed to the server immediately when the tab
backgrounds or closes, instead of relying solely on a 1.5s debounce that could get cut short. - ⏱️ Reminder check tightened from a 60s to a 10s interval, and pushes are now marked
urgency: 'high'— cuts avoidable delay on top of it, though delivery time is ultimately up to
Apple/Google's push relay. - 🪵 Push send failures are now logged instead of silently swallowed.
v1.1.0 — 2026-07-21
- 🐳 Prebuilt Docker images published to
ghcr.io/duartesantos8/opengym-{api,web}(amd64 + arm64)
via GitHub Actions, so self-hosting no longer requires building from source.docker compose pull
grabs them;docker compose up -d --buildstill builds locally if you'd rather. - 🔔 Push notifications: rest-timer-over alert (fires even if the app is closed) and an optional
daily reminder on days you have a workout planned but haven't logged one yet. Opt in per-profile
in Settings — requires a signed-in passkey profile. Backend gains one dependency (web-push);
VAPID keys are generated on first run. - 🐛 Fixed the rest timer stalling when the tab/app is backgrounded — it's now anchored to a real
timestamp instead of a plain per-second counter, so it stays accurate after you come back.
v1.0.0 — 2026-07-20
First public release. A complete, self-hostable gym & body-weight tracker.
Highlights
- ⚖️ Body-weight tracking with an interactive chart + goal line
- 🏋️ Weekly routine planner over 1,324 exercises with animated demos
▶️ Guided workouts: body-weight check-in, pre-filled weights, rest timer, PR detection, per-exercise weight tracking- 🔗 Supersets and 🏃 cardio (time + speed) logging
- 🗓️ Per-day rescheduling without touching your weekly plan
- 🟩 GitHub-style activity heatmap (by time trained)
- 🔑 Passkey (WebAuthn) login with per-profile data that syncs across devices
- 🎨 Light/dark themes + 8 accent colors, synced to your profile
- 📦 JSON export/import, guest mode, PWA install, no telemetry
Stack
- React 19 + Vite (React Router, Zustand)
- Node backend, no framework, single dependency (
@simplewebauthn/server), JSON-file storage - nginx + multi-stage Docker so
docker compose upbuilds and serves everything
Notes
- Exercise media (~140 MB) is fetched from hasaneyldrm/exercises-dataset on first run.
- Licensed under GNU AGPL v3.0.