Files
amethyst/commons/plans/2026-10-06-my-fitness-redesign.md
Claude 8a831c724f fix(fitness): describe the Health Connect history limit correctly
Health Connect's default read limit is 30 days before the app was first
granted access, not a rolling 30 days. The 4-week window is Amethyst's
own choice (no READ_HEALTH_DATA_HISTORY, no stored grant date), so the
dashboard note now says that, and the comments and plan match.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TwdhLj7EsSV7aAETsf34yd
2026-10-06 15:38:11 +00:00

7.8 KiB
Raw Permalink Blame History

My Fitness redesign: goals first, then history

Status: prototype landed (shared dashboard + goals + insights model). Follow-ups listed at the end.

1. Design review of the current screen

The current MyFitnessScreen is well engineered underneath (two-source TrainingLog, progressive Health Connect reads, the "don't flash empty" gating, share-once rules) but the surface is a list of number grids. Findings, roughly by impact:

# Finding Why it matters
1 No goals. The only frame of reference is "+12% vs last week". Numbers without a target don't answer am I doing enough? A week-over-week delta punishes a planned easy week and rewards a one-off.
2 No visuals. Six stacked OutlinedCards of StatGrid text, all the same weight. No hierarchy: the most important answer (on track or not) is not on screen. Trends are invisible when each number is a single point.
3 Rolling 7 days ≠ "this week". WorkoutStats compares the last 7×24h with the 7 before. The window moves every morning. Goals, streaks and "days left" all need a calendar week (locale's first day of week).
4 28-day ceiling on everything. The published log is filtered to WINDOW_DAYS too, though kind 1301s go back indefinitely. "Historical patterns" are impossible with 4 samples per weekday. Only the Health Connect read needs a short window (below).
5 Fasting / diet / meditation count as training time. A 16-hour fast is 960 minutes of "Time" and wins "Longest workout". Corrupts every total, every average and a best effort.
6 Daily streak. currentStreakDays resets on any rest day. Rest days are part of training; a daily streak nudges people to skip them. A weekly goal streak rewards consistency without that.
7 Distance is summed across activities. "Distance 74 km" mixes running and cycling. Meaningless as a number; distance (and pace) only make sense per activity.
8 Redundancy. Workout count appears in This week, the tile row, and the window card. "Last 4 weeks · Weekly average" is jargon. Noise that pushes the useful parts below the fold.
9 Bests have no context. No date, no activity, no "new". A record is a story ("Longest run — Tue, new!"), not a cell.
10 Accessibility. Up/down is carried by colour only (primary vs grey). Fails colour-alone.
11 Platform-locked. All of it lives in amethyst/ while the direction is one UI in commonsUI. Desktop can't render it.

2. Proposed information architecture

Each card answers one question, in the order people ask them:

  1. This week — am I on track? Three goal rings (active minutes, workouts, active days), a row per goal with value of target and a pace label (met / on track / behind, icon + word, never colour alone), an optional per-activity distance bar, a Mon–Sun week strip, and one sentence: "58 min to go · 5 days left". Pace is judged against completed days, so an empty Monday morning is not "behind".
  2. Weekly active time — is it trending right? 12 weeks of bars stacked by activity with a dashed goal line; tap a bar to make it the header. Footer: "Goal met 7 of the last 12 weeks", "Averaging 3h 05m a week lately, +14% vs the 4 weeks before", and an honest note where the weeks predate the 4 weeks Amethyst reads from Health Connect.
  3. Consistency — do I show up? Weekly-goal streak (current + best) and an 18-week calendar heatmap (single-hue ramp: rest / <20 / <45 / <90 / 90+ min).
  4. Your patterns — when do I train? Busiest weekday and usual time of day as tiles, a weekday-average column chart and a time-of-day bar list.
  5. Activity mix — what am I doing more or less of? 100% share bar for the last 4 weeks, and per-activity time per week with the change vs the 4 weeks before.
  6. Personal records across the loaded history, with activity + date and a New badge for anything set this week.
  7. Recent workouts — compact rows with an activity-coloured badge, date, metrics, share icon.

Colour rules (from the dataviz method): activity identity uses the first three slots of the validated categorical palette, fixed by total time over the whole history so a colour follows the activity across charts and never its rank in one week; everything else folds into a neutral "Other". Goal rings take three different slots so a ring never reads as an activity. Single-series charts use the theme primary. No dual axes; thin marks with 4dp rounded data ends and 2dp gaps.

3. What the prototype implements

Piece Where Status
FitnessGoals (weekly minutes / workouts / days + optional per-activity distance; versioned encode/decode) commons/jvmAndroid/fitness/ new
FitnessInsights (calendar weeks, 26-week history, pace, week streaks, heatmap days, weekday/daypart profiles, mix, records, non-movement exclusion) commons/jvmAndroid/fitness/ new, unit-tested
WorkoutStats.bests commons reused (made internal)
Chart primitives: GoalRings, WeeklyBarChart, CalendarHeatmap, ColumnChart, LabeledBar, ShareBar, WeekStrip (Canvas, no new dependency) commonsUI/commonMain/.../workouts/fitness/FitnessCharts.kt new
FitnessDashboard + FitnessGoalsDialog commonsUI/jvmAndroid/.../workouts/fitness/ new, shared (Desktop can render it)
Formatting helpers moved amethyst/.../MyFitnessFormat.kt → commonsUI/.../FitnessFormat.kt extracted
MyFitnessViewModel amethyst/ extended: goals flow, 26-week published window, FitnessInsights
Goal persistence FitnessGoalsPreferences (per-account private SharedPreferences) prototype-grade
Health Connect banner / pending note / connect prompt amethyst/ (header slot) kept
Render test (light + dark, sample half-year) commonsUI/jvmTest/.../FitnessDashboardRenderTest.kt new; MY_FITNESS_RENDER_DIR=… ./gradlew :commonsUI:jvmTest --tests '*FitnessDashboardRenderTest*' writes PNGs

WorkoutStats and its rolling-window Report are untouched (still tested); nothing on the new screen reads them except bests and percentChange.

4. Follow-ups (not in the prototype)

  1. Fetch the user's own history. My Fitness only sees kind 1301s already in LocalCache. Add a one-shot fetchAllPages for kinds=[1301], authors=[me], since=26 weeks on screen open (reuse the accessories/ helpers; don't hand-roll a REQ loop).
  2. Longer Health Connect reads. Health Connect's default limit is 30 days before the app was first granted access, not a rolling 30 days: after a grant, history accumulates. Reading it needs Amethyst to remember its own first-grant date (asking for older data is an error), and costs one aggregate IPC per session. READ_HEALTH_DATA_HISTORY would add the time before the grant. Decided 2026-10-06: keep the 4-week read and no new permissions for now.
  3. Sync goals across devices as an encrypted NIP-78 (kind 30078) app-data event — never a public event; a health target is personal.
  4. Per-activity drill-down: tapping an activity in the mix opens a screen with distance/week, pace or speed trend, and HR trend for that activity only.
  5. Intensity minutes (HR zones) once per-session HR samples are read, so "active minutes" can weight vigorous time ×2 like the WHO guideline does.
  6. Localised date patterns. The prototype uses "MMM d"; use the platform's best pattern (Android DateFormat.getBestDateTimePattern) behind an expect/actual.
  7. Accessibility polish: stepper buttons need content descriptions; per-bar semantics for the trend chart (currently one sentence for the whole chart).
  8. Move the screen shell (MyFitnessScreen's scaffold + Health Connect actions behind a port) into commonsUI so Desktop gets the destination, not just the dashboard.