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

94 lines
7.8 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 `OutlinedCard`s 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.