Skip to content

Releases: DuarteSantos8/openGym

v1.2.2

Choose a tag to compare

@DuarteSantos8 DuarteSantos8 released this 25 Jul 13:07

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 open android/ in Android Studio or ios/ in Xcode —
      see docs/MOBILE.md. NOTICE.md now carries an AGPL §7 app-store exception.
  • 🤖 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.

v1.2.1

Choose a tag to compare

@DuarteSantos8 DuarteSantos8 released this 23 Jul 11:41

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.
  • 🐛 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 to main. 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/ and gif/ next
    to the app) is unchanged.

v1.2.0 - redesign

Choose a tag to compare

@DuarteSantos8 DuarteSantos8 released this 23 Jul 09:59

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

Choose a tag to compare

@DuarteSantos8 DuarteSantos8 released this 22 Jul 13:25

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

Choose a tag to compare

@DuarteSantos8 DuarteSantos8 released this 21 Jul 22:52

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.mjs regenerates the instruction packs from the upstream
    dataset; translations live in frontend/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

Choose a tag to compare

@DuarteSantos8 DuarteSantos8 released this 21 Jul 10:53

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 --build still 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 up builds and serves everything

Notes