Files
workouts/UX-REDESIGN.md
rzen 7cb2d6da26 Show only the day's docket on the Today board and add ad hoc Now workouts
- Due-filter schedules: daily always, fixed days by weekday, one-offs on
  their date; rest days get an empty state
- Workouts with no due schedule row (ad hoc or off-day starts) now render
  as their own board rows
- The + sheet is now "New Workout" with a "When" picker; "Now" (offered
  when adding on today) skips the schedule and starts the workout directly
- Remove the times-per-week scheduling mode everywhere (enum, document,
  entity, mappers, planner, seeds, tests)
2026-07-11 11:09:11 -04:00

229 lines
12 KiB
Markdown

# UX Redesign — Goals, Schedules & the Today Board
Design direction agreed 2026-07-09/10. This restructures the app from a
single-screen history list into a four-tab app built on three independent
layers — routines, schedules, goals — with a **Today board** as the entry
surface, and rides a full visual refresh along with it. The sync architecture
is untouched; the data-model additions are two small new document aggregates
(Goals, Schedules) on the existing machinery.
## The three layers
```
ROUTINES the content — what "Lower Body" or "Evening Meditation" consists of
SCHEDULES the plan — "Lower Body · every Mon & Thu · → Strength"
GOALS the meaning — stable categories everything rolls up to
```
**You plan with schedules, act on routines, observe by goals.** The three are
deliberately independent:
- **Goals outlive plans.** Switching from a 3-day to a 5-day program rewrites
schedules, but the Strength *track* in Progress continues unbroken — history
rolls up to the goal, not to the plan that produced it. The goal is the
ledger category; a schedule is how you're currently trying to fill it.
- **Goals are optional.** A schedule with no goal tag still appears on the
board and logs normally; it rolls up as uncategorized. Pure-scheduling use
works; goals can be adopted later.
- **Scheduling never touches routine files.** The schedule is the join object
(it references `routineID` and `goalID` from its own document), so
scheduling an immutable fixed-ULID starter routine never forks the seed.
### Terminology: "Routine" replaces "Split" (user-facing)
"Split" is lifting jargon; the entity is now general — a meditation session, a
mobility flow, a run. UI strings say **Routine** everywhere; starter splits
become starter routines. Code-level type names (`Split`, `SplitDocument`, …)
stay as-is for now — a rename is churn with no user value; revisit only if it
starts to confuse.
### Goals
Just **name + color/icon + order** — no cadence, no machinery. Examples:
Strength, Endurance, Morning Movement, Mindfulness. Persisted as
`Goals/<ULID>.json` (standard three-place data model). Workouts additionally
**stamp the goal at completion** (denormalized `goalID`/`goalName`, same
pattern as the existing `splitID`/`splitName`), so history keeps the
attribution it had when logged even if schedules are re-tagged later.
### Schedules
A schedule = **one routine + a recurrence + an optional goal tag**, persisted
as `Schedules/<ULID>.json`. Recurrence flavors:
1. **Fixed days** — "every Mon & Thu". Deterministic; the board just shows it.
2. **Daily** — e.g. the morning routine or evening meditation.
(A one-off `.once` recurrence also exists for a single-day plan; a "times per
week, whenever" flexible mode was tried and removed — it added planner
complexity nobody used, and fixed days cover the common case just as well.)
Plus an optional **time window** (e.g. morning, default cutoff ~11 am,
tunable) with a per-schedule setting for whether the window gates the streak:
on = only in-window starts feed the streak, though the row stays startable and
completable all day; off = any-time completion counts.
**Missed fixed days get a catch-up nudge**: the schedule reappears next day as
due-with-a-reason ("missed Mon · catch up?"), planner-placed on the best
make-up day before the next fixed slot (recovery-aware in Phase 2), with an
explicit skip. Skipping just waits for the next fixed slot.
Example of the model's reach: *Goal: Mindfulness ← Schedule: "Meditation ·
daily · evening window" ← Routine: one duration-based Meditation exercise*
(`LoadType.duration` already exists; add a `mindAndBody` case to
`WorkoutActivityType` — optional-int wire field, no schema bump — mapping to
HealthKit's mind-and-body session on the watch).
## Information architecture
Four tabs: **Home · Progress · Library · Settings**.
| Tab | Contents | Provenance |
|---|---|---|
| Home | The Today board (new) | New surface |
| Progress | Per-goal tracks + trends + full history + achievements | Absorbs `WorkoutLogsView`, promotes `WeightProgressionChartView` / `ExerciseProgressView` |
| Library | Exercise library | Promotes `ExerciseLibraryView` out of Settings |
| Settings | Slimmed settings + goal/schedule editing + About | Existing, minus Library |
Routine management stays reachable from Home — setup, not a daily
destination. The `SplitDetailView`/`ExerciseListView` duplication is
consolidated during this restructure (ExerciseListView is a pure duplicate).
The watch app is untouched: it remains a thin remote; nothing changes the wire
format until Phase 2's optional location field.
## Home — the Today board
Home is **the today-projection of the user's schedules**: a flat, stable,
time-ordered checklist where each row wears a small colored goal tag. It is
the jumping board for the day, a guide, and a motivational aid — in that
order. Boxes fill up as the day goes on.
```
Thursday, July 10
┌──────────────────────────────────┐
│ ✓ Mobility ·7:04· [Morning] 🔥12│ done — stays in place, streak ticked
│ ● Lower Body [Strength] │ fixed schedule, due today
│ Mon & Thu · ▶ Start │
└──────────────────────────────────┘
This week ▪▪▪▫▫▫▫ + one encouraging line
```
- **Row states**: *due* (start button, streak at stake — fixed/daily
schedules on their day, catch-ups after a miss) → *in progress* (resume
affordance, always wins attention) → *done* (✓, time, bumped streak). Done
rows stay in place — the agenda's shape is stable and the day visibly fills
up.
- **Due rows carry reason chips** from the planner ("missed Mon", "5 days
since legs · you're usually here Thursdays") — never a black box. An
override is a non-event and becomes signal.
- **Rest is declarable per row**: "make it a rest day" checks a due row off
guilt-free; adherence treats a declared rest as met, and the board stays
completable. (The planner can also propose rest when everything scores low.)
- **Sprinkles**: per-row streak flames, the week strip vs. usual pace, a
clean-sweep moment when the last due row completes, then a small tomorrow
preview.
- **Degrades gracefully**: no schedules → bootstrap proposal card; one
schedule → a one-row board, i.e. a classic single-suggestion dashboard.
## The planner — `NextSplitPlanner`
A **pure, unit-testable scorer** (precedent: `SeedReconcilePlanner`). With
explicit schedules, its scope narrows to where inference is genuinely needed:
- **Catch-up placement** — the best make-up day for a missed fixed slot
before its next occurrence.
- **(Phase 2) Recovery guard** — overlap between a routine's target muscles
and muscles trained in the last ~48 h downweights it, using the bundled
library's `Targets:` metadata (already parsed by `ExerciseInfo`). Because it
looks across *all* recent workouts it is also the cross-goal coordinator
("no heavy legs the morning after a long run").
- **(Phase 2) Location affinity** — boost routines historically done at the
current place; missing/denied location contributes nothing.
Deterministic, hand-tuned weights, fixture-history unit tests.
### Location capture (Phase 2)
- First workout start after the update requests **when-in-use,
reduced-accuracy** location (place-level is all we need). Denial is
respected silently forever.
- A coarse fix is stamped on `WorkoutDocument` as an **optional field** — no
schema bump; older decoders ignore it (same rationale as `activityType`).
- Places are **derived at read time** by proximity clustering (~150 m);
clusters are never persisted — consistent with the rebuildable-cache
philosophy.
## Streaks & adherence
- **Computed per schedule**: did "Lower Body" hit Mon & Thu this week; did
"Meditation" happen daily. Declared rests count as met. Streak state is
always **derived** from workout documents — never persisted.
- **Rolled up per goal**: a goal's streak = consecutive weeks in which *all*
its member schedules hit their recurrence. Precise diagnostics ("Upper is
what's slipping") with an honest rollup.
- Week-based schedules are forgiving by construction: a missed day breaks
nothing until the week itself is missed.
## Progress
- **Per-goal tracks first**: each goal's adherence over time, fed by
per-schedule adherence; then aggregate trends — weekly volume, workout
frequency; muscle-group balance (Phase 2). Time ranges per the
trend-visualization pattern (7D/30D/90D/1Y/All).
- **Per-exercise drill-down**: the existing weight-progression and exercise
progress views, promoted and restyled; later pinnable to a goal as an
optional **outcome metric** (observed, never promised — goals themselves
stay practice-anchored).
- **History**: the current `WorkoutLogsView` list lives here.
- **Achievements section**: a badge grid within Progress. All achievements are
**pure derivations** (per-goal streak milestones, PR counts, volume/session
milestones) — recomputable after any cache rebuild. Persisting earned-dates
is deferred until we want one-time celebration moments.
## Visual redesign
Rides Phase 1 rather than following it: a shared component set — card, stat
tile, board row (due / ambient / done variants), goal tag, badge, reason chip
— with the iOS 26 glass treatment, applied as each surface is built. Existing
screens (run flow, routine editing) are restyled to the same system as they're
touched. Theming continues through `Color+Extensions`.
## Phasing
- **Phase 1 — goals + schedules + shell + design system**: Goal and Schedule
document aggregates (documents + cache entities + mappers + editing UI +
activity-type **bootstrap** that proposes initial goals/schedules from
existing routines); 4-tab `TabView`; Today board v1 (due/ambient/done rows,
catch-up nudges, resume, declared rest); planner v1 (recency + even-spacing
placement, weekday affinity); Progress tab (per-goal tracks + history +
promoted charts + achievements grid); Library promoted; Settings slimmed;
"Routine" terminology pass; view-duplication consolidation; component set.
- **Phase 2 — the senses**: location capture + place affinity, recovery guard
(cross-goal coordination), muscle-balance chart, outcome-metric attachments,
`mindAndBody` activity type + watch session mapping, richer achievements.
- **Phase 3 — the library grows**: external exercise ingestion — exercises
become user documents (new aggregate alongside the bundled library; real
schema work). Library UI is designed now to accommodate this.
## Open questions
- Tab icons/names final pass (Home/Progress/Library/Settings working titles).
- **How declared rest / explicit skip persists.** Purely local UI state won't
survive device switches or cache rebuilds. Candidate: a `WorkoutDocument`
with `status: .skipped` for that schedule's routine (the status already
exists) — cross-device and derivation-friendly. Decide before Phase 1 build.
Related: distinguish *abandoned* (never started / walked away) from an
*explicit skip*, possibly carrying an "excuse" ("sick day") that adherence
treats as met. The board's past-day rows currently show an undifferentiated
"Skipped" tag as the placeholder for this.
- Where goal/schedule editing lives: Settings vs. from the board (long-press
a row / "edit plan" affordance) vs. both.
- Clean-sweep celebration + tomorrow-preview treatment (how loud, how playful).
- Default morning cutoff: fixed ~11 am vs. learned from the user's own
distribution.
- Whether the watch should eventually show the Today board / row states
(would need the computed projection pushed in the application context —
cheap; revisit after Phase 1).
- How far the "practice tracker" identity goes (app is named *Workouts*;
mindfulness fits the model today — does the branding follow eventually?).