Skip to content
22 changes: 15 additions & 7 deletions .claude/commands/backlog.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
---
description: Add to or prioritize the backlog
model: sonnet
effort: low
---

# Backlog Management
Expand All @@ -26,15 +25,24 @@ This command routes to the build agent:
## Workflow

### Step 1: Determine action
The first parameter ($1) indicates what action to take:
If $ARGUMENTS is empty, default to `list` (show all open backlog items).

Otherwise, the first parameter ($1) indicates what action to take:
- `list` — Show all open items from Ideas.md and Bugs.md (default when no args)
- `add` — Add a new item to Ideas.md or Bugs.md
- `prioritize` — Order existing items by priority
- `move` — Move items between Ideas and Bugs
- `remove` — Remove resolved items

Additional details provided via $ARGUMENTS.

### Step 2: For `add` action
### Step 2: For `list` action (default)
1. Load Context/Backlog/Ideas.md and Context/Backlog/Bugs.md
2. Display all open items, grouped by file (Ideas / Bugs)
3. Show priority level if present
4. Suggest next actions: add an item, prioritize, or start planning a high-priority item

### Step 3: For `add` action
1. If $ARGUMENTS provided, it contains the item description
2. If no $ARGUMENTS, ask user for the item description
3. Determine if item is an idea or a bug
Expand All @@ -45,27 +53,27 @@ Additional details provided via $ARGUMENTS.
- Initial priority (optional)
6. Confirm entry was added

### Step 3: For `prioritize` action
### Step 4: For `prioritize` action
1. Load both Ideas.md and Bugs.md
2. Present current items
3. Ask user to reorder by priority
4. Optionally ask for priority levels (high/medium/low)
5. Save reordered lists

### Step 4: For `move` action
### Step 5: For `move` action
1. Load both backlogs
2. Present items
3. Ask which item to move and to which file
4. Move item between files

### Step 5: For `remove` action
### Step 6: For `remove` action
1. Load appropriate backlog file
2. Present items
3. Ask which to remove
4. Remove entry and save file
5. Optionally ask to archive to Done/ instead

### Step 6: Summary
### Step 7: Summary
Present the updated backlog and suggest next steps:
- Start planning a high-priority item
- Continue current implementation
Expand Down
33 changes: 12 additions & 21 deletions Context/Backlog/Ideas.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,11 @@ skill. Prioritize via the backlog-prioritize skill.
**Source:** `Context/Reviews/0009-pr71-enhancement-batch-review.md`
**Resolution:** TauriAdapter.getUnreadCounts already has 2 unit tests (happy path + empty). The `Promise.allSettled` batching was a suggestion for SupabaseAdapter that was never implemented (still sequential loop). No further action needed; re-open if batching is implemented.

## Aggregated 1RM View
## ~~P11-012: Aggregated 1RM View~~ (Resolved 2026-04-22)

**Added:** 2026-04-05
**Source:** `Context/Reviews/0011-pr76-web-responsiveness-review.md` (P11-012)
**Priority:** Medium

The `OneRmManagement` component was removed from the profile page during the responsiveness overhaul. The per-exercise 1RM management is still accessible on each exercise detail page (`/exercises/$exerciseId`), but the aggregated cross-exercise 1RM view no longer exists. Consider adding an aggregated 1RM summary to the library exercises tab or a dedicated stats/progress page.
**Resolution:** Restored aggregated 1RM view in Vault 1RM tab -- commit `7fd07f2`.

### ~~Overhaul web responsiveness for larger screens~~ (Resolved 2026-04-05)

Expand All @@ -83,30 +81,23 @@ The `OneRmManagement` component was removed from the profile page during the res
**Added:** 2026-04-05
**Resolution:** Reordered sidebar nav into logical groups: core tools, reference, social. Commit `1b359d2`.

### Exercise completion signal during active workout
### ~~Exercise completion signal during active workout~~ (Resolved 2026-04-22)

**Added:** 2026-04-12
**Source:** User feedback
**Priority:** Medium

Two related UX gaps in the workout logger:
**Resolution:** Implemented in PR #113 (F023) -- exercise completion signal shipped.

1. **"Done with this exercise" affordance** -- No explicit way to mark an exercise block as finished mid-workout. Users who finish squats before the planned sets are complete (injury, RPE ceiling, time) have no way to signal they're moving on. Options: a "Mark complete" button per exercise block, a long-press gesture, or a swipe action that collapses the block and flags it as done.
### ~~"Add exercise" pre-populated with frequently used exercises~~ (Resolved 2026-04-22)

2. **Trailing auto-populated set ambiguity** -- When the logger auto-populates a new set row, it's unclear whether exiting the exercise (or ending the workout) will count that empty row as a logged set or discard it. Users are left guessing whether their volume is being over-counted. Need either: discard empty trailing sets on exit, or show explicit "incomplete set" vs "complete set" state that is visible before saving.
**Added:** 2026-04-12
**Resolution:** Implemented in PR #114 (F024) -- frequent exercises add picker shipped.

**Related:** `src/routes/_authenticated/log.$workoutId.tsx`, workout logger state, set auto-population logic
**Area:** Workout logger UX
### ~~P22-014: Move view-local modal state into StrengthWorkoutView~~ (Resolved 2026-04-22)

### "Add exercise" pre-populated with frequently used exercises
**Added:** 2026-04-21
**Resolution:** Implemented in commit `689548b` -- modal state encapsulated inside `StrengthWorkoutView`.

**Added:** 2026-04-12
**Context:** When opening the "Add exercise" picker, the search field starts empty with no suggestions. Pre-populating it with the user's most frequently logged exercises (derived from workout history) would reduce search friction, especially on the gym floor where speed matters.
**Related:** Exercise picker/search component, workout history data, active workout flow
**Priority:** Medium
### ~~Split log.$workoutId route into three components~~ (Resolved 2026-04-21)

### Split log.$workoutId route into three components
**Added:** 2026-04-06
**Context:** `src/routes/_authenticated/log.$workoutId.tsx` is ~800 lines with three distinct rendering paths (event workout, strength workout, post-workout summary) toggled by conditional branches. Splitting into separate route components would clarify ownership, shrink the file, and isolate state hoisting per path.
**Related:** `src/routes/_authenticated/log.$workoutId.tsx`
**Resolution:** Implemented in PR #115 -- route split into `EventWorkoutView` and `StrengthWorkoutView` child components.
**Priority:** Medium
42 changes: 42 additions & 0 deletions Context/Features/023-Exercise-Completion-Signal/Steps.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,48 @@

---

### Phase 6: Review Follow-up Tasks (P22 batch)

- [x] S008-T: Add test file for `StrengthWorkoutView` covering the finish guard
behavior. Three paths required: (1) no dirty rows -- `handleFinish` called
directly without showing dialog; (2) dirty row present -- `showFinishDirtyDialog`
displayed; (3) dirty row confirmed via dialog -- subsequent Finish proceeds
without re-showing dialog. Also verify `pendingDirty` is cleared when
`onSkipExercise` fires (P22-009 regression guard).
- **Assigned:** builder-ui
- **Depends:** none
- **Parallel:** true

- [x] S009-T: Extend `src/components/workout/__tests__/set-row.test.tsx` with
`onPendingDirty` prop tests. Required assertions: (1) called exactly once on
first weight or reps edit of a pending row; (2) not called on a confirmed
(read-only) row; (3) not called a second time on subsequent field edits of the
same pending row.
- **Assigned:** builder-ui
- **Depends:** none
- **Parallel:** true

- [x] S010-T: Add test file for `EventWorkoutView`. Cover: (1) discard dialog
triggered via `WorkoutPausedBar.onDiscard`, Cancel path keeps the workout
active, Discard path calls `handleDiscard`; (2) `canFinish` is always `true`
(no set-count gate); (3) `pageError` set externally renders `ErrorBanner` and
is dismissible.
- **Assigned:** builder-ui
- **Depends:** none
- **Parallel:** true

- [x] S011-T: Add a mixed CIRCUIT + STRENGTH scenario to
`src/routes/_authenticated/__tests__/-log-workout-finish-banner.test.tsx`.
Scenario: one CIRCUIT group with incomplete activities plus one STRENGTH group
where all activities are in `skippedActivityIds`. Assert `allActivitiesDone`
is `true` and the "READY TO FINISH?" banner is visible. This guards against
regression of the CIRCUIT exclusion filter in `allActivitiesDone`.
- **Assigned:** builder-ui
- **Depends:** S007-T
- **Parallel:** true

---

## Acceptance Criteria

- [ ] All 8 testable assertions from Spec.md verified (A-001 through A-008)
Expand Down
102 changes: 102 additions & 0 deletions Context/QuickPlans/Done/2026-04-21-exercise-completion-signal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# Quick Plan: Exercise Completion Signal

**Created:** 2026-04-21
**Source:** Backlog -- "Exercise completion signal" (Ideas.md)

---

## Task

Resolve two related UX gaps in the strength workout logger:

1. **"Done with this exercise" affordance** -- Users need a discoverable way to mark an exercise block finished before all planned sets are complete.
2. **Trailing set ambiguity** -- The auto-populated pending input row looks identical to a confirmed set row. Users can't tell if exiting/finishing will save or discard it.

---

## Context (from code exploration)

| Finding | Detail |
|---|---|
| Done button | `ExerciseBlock` already renders a "Done" button when `onSkipExercise` prop is provided (`exercise-block.tsx:208-228`). `StrengthWorkoutView` wires `handleMarkDone` → `skipActivity` which adds the activity ID to `skippedActivityIds`. Block then collapses. Users can re-expand via expand button. |
| Pending row rendering | A pending input row renders in `strength-workout-view.tsx:331-376` when (a) no confirmed sets exist yet, OR (b) `pendingInputs[activity.id]` is `true` (set by "Add set" button). |
| Trailing row on finish | `handleFinish` (log.$workoutId.tsx:280-338) clears `pendingInputs` before calling `finishWorkout()`. The unconfirmed row is discarded -- never persisted. No DB overcounting. |
| Discoverability gap | The "Done" button is at the bottom of an expanded exercise block. On a long exercise block with many sets, it may be below the fold on mobile. No visual cue distinguishes a pending (unconfirmed) input row from confirmed set rows except that it has editable fields. |

**Net scope:** Gap 1 is largely solved at the code level -- the wiring exists. The work is UX polish: ensure the Done button is discoverable. Gap 2 needs a pending-row visual indicator and/or a finish-time guard when the user has typed values into a pending row.

---

## Goal

- Done button is clearly discoverable and accessible without scrolling on typical exercise blocks.
- Pending input rows are visually distinct from confirmed set rows so users know they are "not yet logged."
- If a user hits Finish with a non-empty pending row (weight or reps typed), surface a confirmation or auto-discard notice so nothing is silently lost.

---

## Approach

### 1. Pending row visual indicator (`exercise-block.tsx` + `set-row.tsx`)

Add a visual "PENDING" or "UNLOGGED" state to the pending input row:

- Add an `isPending?: boolean` prop to `SetRow`.
- When `isPending` is true, render the row with a muted/dimmed style (e.g., `opacity-60` or `bg-surface-pit/40`) and a small ALL-CAPS label (`PENDING` or `IN PROGRESS`) in the status column instead of the confirm checkmark column.
- Pass `isPending={true}` to the `SetRow` rendered for the pending input in `strength-workout-view.tsx:370-376`.

This makes it immediately obvious which row is "not yet logged."

### 2. Finish-time guard for non-empty pending rows (`strength-workout-view.tsx` + `log.$workoutId.tsx`)

In `handleFinish` (log.$workoutId.tsx:280), before clearing `pendingInputs`, check if any activity has a pending row with non-empty weight or reps. If so, surface a brief confirmation dialog or toast: "You have an unconfirmed set -- it won't be saved. Finish anyway?"

Implementation detail:
- `pendingInputs` is a `Record<activityId, boolean>` -- it only tracks whether to show the row, not the current input values.
- The actual weight/reps input values live in local state inside each `SetRow` component, making them inaccessible from `handleFinish`.
- **Simpler approach:** Lift a `pendingSetValues` map (activityId → `{weight, reps}`) up to `StrengthWorkoutView` state, updated on every input change via a callback prop on `SetRow`. Then `handleFinish` can inspect it.
- If lifting state is too invasive, use a ref-based approach: a `pendingHasValues` Set stored in a ref, updated via an `onPendingChange` callback from `SetRow`.

### 3. Done button discoverability (`exercise-block.tsx`)

The "Done" button is already rendered at block bottom (line 208-228). Evaluate if it needs a sticky or pinned position when the block is long. Options:
- Keep current position (bottom of block) -- acceptable if most blocks have ≤ 5 sets.
- Add a secondary collapsed-header affordance: a small "DONE" badge/button in the exercise block header row that dismisses without scrolling.

Recommendation: Start with option A (no change to position, verify in app). Only add header affordance if gym-floor testing shows it's hard to reach.

---

## Files to Touch

| File | Change |
|---|---|
| `src/components/workout/set-row.tsx` | Add `isPending?: boolean` prop; conditional dim + PENDING label in status column |
| `src/components/workout/exercise-block.tsx` | Pass `isPending` to the pending `SetRow`; optionally expose `onPendingChange` callback |
| `src/routes/_authenticated/log.$workoutId/strength-workout-view.tsx` | Track `pendingSetValues` state; pass `isPending` and `onPendingChange` to relevant rows |
| `src/routes/_authenticated/log.$workoutId.tsx` | Guard in `handleFinish`: if any pending row has values, show confirmation before finishing |

---

## Verification

- [ ] Pending input row is visually distinct from confirmed rows (muted styling + PENDING label visible)
- [ ] Confirming a set clears the PENDING state and shows the row as confirmed
- [ ] Finishing with a non-empty pending row shows a confirmation prompt
- [ ] Finishing with an empty pending row finishes immediately (no false positive prompt)
- [ ] Done button collapses the exercise block and it appears in collapsed/done state
- [ ] Collapsed block can be re-expanded to add more sets
- [ ] No regressions in set confirmation or workout finish flow

---

## Risks

- **Lifting pending values state** could be moderately invasive if `SetRow` doesn't currently support callbacks for every keystroke. Check current `SetRow` props and local state before deciding between lifted state vs ref approach.
- **False positive guard triggers** if the empty pending row pre-fills with prescribed weight/reps. Guard should check if values differ from the initial pre-filled defaults, OR check if the user has actually interacted with the row (dirty flag).

---

## Execution

`/impl` -- moderate scope, 3-4 files, no cross-stack coordination needed.
49 changes: 49 additions & 0 deletions Context/QuickPlans/Done/2026-04-21-p11-012-aggregated-1rm-view.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Quick Plan: Aggregated 1RM View (P11-012)

**Date:** 2026-04-21
**Backlog Source:** `Context/Backlog/Ideas.md` -- P11-012

---

## Task

Surface the aggregated cross-exercise 1RM view that was removed from the profile page during the PR #76 responsiveness overhaul.

## Goal

The Vault's 1RM TRENDS tab already shows per-exercise historical charts (`VaultOneRmTab`). Add the aggregated current-max summary (`OneRmManagement`) above the trend selector so athletes can see all their 1RMs at a glance and drill into a trend by clicking an exercise.

## Current State

| Component | Location | Status |
|---|---|---|
| `OneRmManagement` | `src/components/profile/one-rm-management.tsx` | Exists, not surfaced anywhere |
| `VaultOneRmTab` | `src/components/vault/vault-one-rm-tab.tsx` | Live in Vault "1RM TRENDS" tab |
| `VaultOneRmTab` | `src/routes/_authenticated/vault.tsx` | Wired as `value="one-rm"` tab |

## Approach

Enhance `VaultOneRmTab` to show two sections:

1. **Aggregated summary** (top) -- wire `OneRmManagement` using profile data. Clicking an exercise row pre-selects it in the trend selector (optional UX improvement).
2. **Trend chart** (below) -- existing exercise selector + `OneRmChart` (unchanged).

**Data requirement:** `OneRmManagement` needs `exerciseMaxes: Record<string, OneRepMax>` and `preferredUnits` from user profile. Add `useUserProfile(userId)` to `VaultOneRmTab` to supply these props.

**Files to touch:**
- `src/components/vault/vault-one-rm-tab.tsx` -- add `useUserProfile`, render `OneRmManagement` above trend section

No other files need to change. `OneRmManagement` is already a complete component.

## Verification

- Vault > 1RM TRENDS tab shows list of all exercises with current 1RM values
- "Update" button in aggregated list opens the edit dialog (inherited from `OneRmManagement`)
- Selecting an exercise in the trend selector still shows the chart below
- Empty states work: no maxes recorded, no exercises with `supports1RM`
- Loading skeleton covers both sections

## Risks

- `useUserProfile` may return `exerciseMaxes` as `undefined` before load -- need null-safe default (`exerciseMaxes ?? {}`)
- If user has no 1RM data, `OneRmManagement` renders its own `EmptyState` -- the double empty state (one from `VaultOneRmTab`, one from `OneRmManagement`) must be reconciled: let `OneRmManagement` own the empty state and suppress the outer one
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# P22-014: Move View-Local Modal State into StrengthWorkoutView

**Date:** 2026-04-21
**Scope:** Refactor (2 files)
**Source:** P22 review finding from PR #115

---

## Task

Move `showAddExercise`, `showDiscardDialog`, and `pageError` state from the parent route
(`ActiveWorkoutPage`) into `StrengthWorkoutView`, where they are exclusively consumed.

---

## Goal

Reduce prop-threading in `ActiveWorkoutPage`. These three state pairs are UI concerns
internal to `StrengthWorkoutView` -- the parent has no reason to own or read them.
The component's prop interface shrinks from 43 to 37 props after removal.

---

## Approach

### 1. Add local state to `StrengthWorkoutView`

In `strength-workout-view.tsx`, add three `useState` declarations:

```tsx
const [showAddExercise, setShowAddExercise] = useState(false)
const [showDiscardDialog, setShowDiscardDialog] = useState(false)
const [pageError, setPageError] = useState<string | null>(null)
```

### 2. Remove the six props from `StrengthWorkoutViewProps`

Delete from the interface:
- `showAddExercise: boolean`
- `setShowAddExercise: (v: boolean) => void`
- `showDiscardDialog: boolean`
- `setShowDiscardDialog: (v: boolean) => void`
- `pageError: string | null`
- `setPageError: (v: string | null) => void`

### 3. Clean up `log.$workoutId.tsx`

- Remove the three `useState` declarations (lines 113-114, 116) from `ActiveWorkoutPage`
- Remove the six prop assignments from the `<StrengthWorkoutView ... />` JSX (lines 549-554)
- Verify `showSummary` and `restMinimized` are untouched (they remain at parent level)

---

## Verification

- `bun run build` passes (TypeScript confirms prop interface is clean)
- `bun run lint` passes
- Manual smoke test: open an active strength workout, add an exercise, trigger discard dialog, verify both modals open/close correctly and page errors surface as before

---

## Risks

- `pageError` is set in several places inside `StrengthWorkoutView` (lines 147, 292, 325, 410, 419) -- all internal, so no external setter call sites should remain after removal. Confirm with a grep for `setPageError` in `log.$workoutId.tsx` after the change.
- `restMinimized` is a candidate for the same treatment but has been intentionally deferred -- do not include it in this task.
Loading