- 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)
229 lines
12 KiB
Markdown
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?).
|