← Back to updates

Stats with shape.

v1.6.3 redrew the Home screen so it reflects what you actually train. This release picks up the same thread one tab over: History becomes Stats, with a redesigned Overview that gives your progress a visible shape — a 12-week consistency heatmap, the newest PRs across every movement, a per-exercise chart you can drill into, and a 30-day-vs-prior-30 comparison once you have enough history to compare.

Three big threads landed: the Stats tab redesign, per-exercise drilldown with Swift Charts, and a foundational Movement table on the backend that makes durable per-exercise queries possible for the first time. Plus a deep-link from "Calibrate a skill" straight into the calibration program, and a handful of smaller polish.

History becomes Stats

The History tab was a flat chronological list of completed runs — useful for "what did I do on Tuesday", much less so for "am I getting stronger?" v1.6.4 splits it in two via a segmented control at the top:

  • Overview — the new layout. A compact stats row (workouts this month, current streak, best streak, total time), a 12-week consistency heatmap, recent PRs, skill progress (when the data lands), and a 30-day compare card.
  • Runs — the original chronological list, untouched. Tap a run to drill into per-exercise results. Filter and swipe-to-delete still work the same.

The tab itself is renamed in the bottom bar from History to Stats to match what the Overview actually shows.

Heatmap, Recent PRs, and Compare

Three sections do the heavy lifting on the Overview:

  • Consistency heatmap — 12 columns × 7 rows of cells, one cell per day in your local timezone, teal intensity scaling with how many workouts you logged that day. GitHub-style. Backend buckets your workouts in whichever IANA timezone iOS sends, so a 23:30 session in Stockholm lands on the right day even if it crosses UTC midnight. When the backend isn't reachable the card falls back to a client-side derivation from your local history list — you always see your training pattern.
  • Recent PRs — newest personal bests across every movement you've trained. One row per movement showing the previous best vs. current, sorted by when the PR was set. Tap any row to open the per-movement drilldown. Empty for new users with a calm "No PRs yet" message that points them to per-workout PRs in Runs.
  • Compare — last 30 days vs. the prior 30. Workouts, total volume, average session length, each with up/down deltas. Locked behind a 60-day-of-training threshold with a progress bar that ticks toward unlock — comparing windows when you've only trained 17 days isn't comparing anything yet.

Per-exercise drilldown

Tapping an exercise row inside any past workout now opens a dedicated drilldown:

  • Big headline number: best estimated 1RM for weighted exercises, top reps in a session for bodyweight
  • 30-day trend chip in teal when you're up, muted when flat or down
  • Swift Charts line of the metric over time, with PR diamonds at the sessions where you set a new max
  • Last 10 sessions in a monospace list with the PR-set marked

The visual language is the same as the Strength Profile chart in the Profile tab — teal line, diamond markers — so the two views feel like siblings rather than competing aesthetics.

Movement table — the backend foundation

This part is mostly invisible from the app but it's what makes everything above durable. Per-exercise history queries used to join on ProgramExercise.name string-matching, which was fragile across catalog rebuilds, custom programs, and renames. v1.6.4 introduces a master Movement table — one row per abstract exercise (Pull-Up, Decline DB Bench, Front Lever hold), regardless of which program prescribes it. UserExerciseProgress rows now carry a denormalized movementId index, so the per-movement drilldown reads from one indexed column instead of three joined tables.

Practically: "all my Decline DB Bench sessions" now means all of them — across the strength split, any custom program you've made, and any future program that uses the same movement. Catalog renames don't break history. A movementKey field also bridges the new table to the existing UserMax calibration data, so a session PR on Bench Press shares lineage with a Bench calibration test if both are tagged with the same key.

The migration ships nullable FK columns alongside a backfill script, and the seed script auto-derives Movements from exercises.json on every re-seed — no manual catalog upkeep.

"Calibrate a skill" deep-links into the program

The empty Skill-progress section on Stats Overview has a CTA: Calibrate a skill · Start ›. Previously this dumped you on the Programs tab with the Recent filter, which is the wrong filter for finding the calibration test. Now it deep-links directly into the Strength Calibration program's Start screen — one tap from beginning the test. Falls back gracefully to the Programs tab with the Test filter pre-selected if the catalog isn't loaded yet.

Smaller fixes

  • Heatmap fallback — if the new /heatmap endpoint is unreachable for any reason (stale deploy, network blip), the card derives from your existing history list instead of going empty.
  • Per-section empty states — every section on Stats Overview has its own inline empty when data isn't there yet (no PRs, no skill calibration, <60 days of history), so the page teaches the layout to new users without dead space.
  • NetworkRequest query params — internal: every iOS request struct could declare queryItems but the URL builder ignored them. Now properly attached, so ?days=84&tz=Europe/Stockholm actually reaches the server.
  • Zero-state Stats — brand new users (zero workouts) see a soft welcome with a ghost-preview of the heatmap so they know what to expect after their first workout.

The thread tying this release together: training progress should have a shape you can see. A heatmap shows consistency. A line chart shows strength. A PR row shows growth. Stats becomes the tab you open when you want to know if the work is working.

Email me if anything feels off — support@zenmotion.app. Every message still reaches the person who wrote the code.

— Jacob