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
7.8 KiB
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:
- 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".
- 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.
- 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).
- 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.
- 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.
- Personal records across the loaded history, with activity + date and a New badge for anything set this week.
- 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)
- Fetch the user's own history. My Fitness only sees kind 1301s already in
LocalCache. Add a one-shotfetchAllPagesforkinds=[1301], authors=[me], since=26 weekson screen open (reuse theaccessories/helpers; don't hand-roll a REQ loop). - 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_HISTORYwould add the time before the grant. Decided 2026-10-06: keep the 4-week read and no new permissions for now. - Sync goals across devices as an encrypted NIP-78 (kind 30078) app-data event — never a public event; a health target is personal.
- 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.
- Intensity minutes (HR zones) once per-session HR samples are read, so "active minutes" can weight vigorous time ×2 like the WHO guideline does.
- Localised date patterns. The prototype uses
"MMM d"; use the platform's best pattern (AndroidDateFormat.getBestDateTimePattern) behind an expect/actual. - Accessibility polish: stepper buttons need content descriptions; per-bar semantics for the trend chart (currently one sentence for the whole chart).
- Move the screen shell (
MyFitnessScreen's scaffold + Health Connect actions behind a port) intocommonsUIso Desktop gets the destination, not just the dashboard.