Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ Three entry types supported via `entryType` field (default: `cycling`):
- **`rest`** — rest day logging (Recovery Metrics + Rest Day sections only); auto-expands Recovery Metrics on switch
- **`other`** — non-cycling activities like MFR/yoga/strength (Activity section only)

`goal`, `rpe`, `feel` are only required when `entryType === "cycling"` (enforced by `superRefine`). Switching entry types does not reset other fields — hidden fields retain their values but don't appear in markdown output.
`goal`, `rpe`, `feel` are only required when `entryType === "cycling"` (enforced by `superRefine`). `feel` is also optional for rest entries and appears in rest markdown as `F:`. Switching entry types does not reset other fields — hidden fields retain their values but don't appear in markdown output.

Old drafts (pre-entryType) are migrated to `cycling` on restore in `use-form-persistence.ts`.

Expand All @@ -119,15 +119,15 @@ Old drafts (pre-entryType) are migrated to `cycling` on restore in `use-form-per
Always required: `entryType` (`cycling`/`rest`/`other`), `workoutDate`
Cycling-required (via `superRefine`): `goal`, `rpe` (1-10), `feel` (W/P/N/G/S)
Cycling-optional: `choIntakePre`, `choIntake`, `choIntakePost`, `normalizedPower`, `tss`, `avgHeartRate`, `hrv`, `rMSSD`, `rhr`, `trainerRoadRpe`, `trainerRoadLgt`, `whatWentWell`, `whatCouldBeImproved`, `description`
Rest-only: `weight` (positive number, kg), `restNotes` (free-form, rendered as bullets). Rest entries also reuse `hrv`/`rMSSD`/`rhr`/`trainerRoadLgt`.
Rest-only: `weight` (positive number, kg), `restNotes` (free-form, rendered as bullets). Rest entries also reuse `hrv`/`rMSSD`/`rhr`/`trainerRoadLgt`/`feel`.
Other-only: `activityGoal` (e.g. "MFR", "Yoga"), `activityNotes` (free-form, rendered as bullets).

### Markdown Abbreviations
G=Goal (cycling) or Activity (other), R=RPE, F=Feel, Ci-Pre=Carbohydrate Intake Pre-Workout, Ci=Carbohydrate Intake During Ride, Ci-Post=Carbohydrate Intake Post-Workout, NP=Normalized Power, TSS=Training Stress Score, Hr=Heart Rate, HRV=Heart Rate Variability, rMSSD=HRV Recovery Metric, RHR=Resting Heart Rate, TR-RPE=TrainerRoad RPE, TR-LGT=TrainerRoad Light, Weight (rest — written out in full)

### Markdown Output Per Entry Type
- **cycling**: `G` / `R` / `F` + optional metrics + WWW/WCBI/Planned blocks (current format, unchanged)
- **rest**: `Rest Day` marker + present-only recovery metrics and `Weight` + bulleted `restNotes`
- **rest**: `Rest Day` marker + present-only recovery metrics, `Weight`, `F` (feel if set) + bulleted `restNotes`
- **other**: optional `G: <activity>` + bulleted `activityNotes`; no metrics

## Environment Variables (Deployment Only)
Expand Down
34 changes: 34 additions & 0 deletions client/src/pages/home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ function generateRestMarkdown(data: InsertWorkout): string {
markdown += `TR-LGT: ${data.trainerRoadLgt}\n`;
}
if (data.weight) markdown += `Weight: ${data.weight}\n`;
if (data.feel) markdown += `F: ${data.feel}\n`;

if (data.restNotes) {
markdown += '\n' + formatBulletPoints(data.restNotes) + '\n';
Expand Down Expand Up @@ -1297,6 +1298,39 @@ export default function Home() {
)}
/>

<FormField
control={form.control}
name="feel"
render={({ field }) => (
<FormItem>
<FormLabel className="flex items-center gap-2">
<Smile className="w-4 h-4 text-green-500" />
F (Feel): {feelOptions.find(opt => opt.value === field.value)?.label || 'Normal (N)'}
</FormLabel>
<FormControl>
<div className="px-2 py-4">
<Slider
value={[feelOptions.findIndex(opt => opt.value === field.value) + 1]}
onValueChange={(value) => field.onChange(feelOptions[value[0] - 1]?.value || 'N')}
max={5}
min={1}
step={1}
className="w-full"
/>
<div className="flex justify-between text-xs text-gray-500 mt-2">
<span>Weak</span>
<span>Poor</span>
<span>Normal</span>
<span>Good</span>
<span>Strong</span>
</div>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>

<FormField
control={form.control}
name="restNotes"
Comment on lines +1301 to 1336
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 When feel is undefined (the user hasn't moved the slider on a rest entry), feelOptions.findIndex(...) returns -1, making value={[0]}. Because min={1}, Radix clamps the thumb to position 1 (Weak) — yet the label fallback shows "Normal (N)". The thumb and label disagree, misleading the user about what value (if any) will be written to the markdown. The cycling slider never encounters this because feel is required there; rest entries need an explicit "not set" state.

Suggested change
<FormField
control={form.control}
name="feel"
render={({ field }) => (
<FormItem>
<FormLabel className="flex items-center gap-2">
<Smile className="w-4 h-4 text-green-500" />
F (Feel): {feelOptions.find(opt => opt.value === field.value)?.label || 'Normal (N)'}
</FormLabel>
<FormControl>
<div className="px-2 py-4">
<Slider
value={[feelOptions.findIndex(opt => opt.value === field.value) + 1]}
onValueChange={(value) => field.onChange(feelOptions[value[0] - 1]?.value || 'N')}
max={5}
min={1}
step={1}
className="w-full"
/>
<div className="flex justify-between text-xs text-gray-500 mt-2">
<span>Weak</span>
<span>Poor</span>
<span>Normal</span>
<span>Good</span>
<span>Strong</span>
</div>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="restNotes"
<FormField
control={form.control}
name="feel"
render={({ field }) => (
<FormItem>
<FormLabel className="flex items-center gap-2">
<Smile className="w-4 h-4 text-green-500" />
F (Feel): {feelOptions.find(opt => opt.value === field.value)?.label || 'Not set'}
</FormLabel>
<FormControl>
<div className="px-2 py-4">
<Slider
value={[Math.max(1, feelOptions.findIndex(opt => opt.value === field.value) + 1)]}
onValueChange={(value) => field.onChange(feelOptions[value[0] - 1]?.value || 'N')}

Expand Down
13 changes: 11 additions & 2 deletions client/src/test/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ function generateRestMarkdown(data: InsertWorkout): string {
markdown += `TR-LGT: ${data.trainerRoadLgt}\n`;
}
if (data.weight) markdown += `Weight: ${data.weight}\n`;
if (data.feel) markdown += `F: ${data.feel}\n`;

if (data.restNotes) {
markdown += '\n' + formatBulletPoints(data.restNotes) + '\n';
Expand Down Expand Up @@ -249,7 +250,6 @@ describe('generateMarkdown — rest output', () => {
...baseRest,
goal: 'leaked goal',
rpe: 7,
feel: 'S',
normalizedPower: 200,
tss: 50,
choIntake: 'gel',
Expand All @@ -258,14 +258,23 @@ describe('generateMarkdown — rest output', () => {
});
expect(md).not.toContain('G:');
expect(md).not.toContain('R:');
expect(md).not.toContain('F:');
expect(md).not.toContain('NP:');
expect(md).not.toContain('TSS:');
expect(md).not.toContain('Ci:');
expect(md).not.toContain('WWW');
expect(md).not.toContain('WCBI');
});

it('includes feel when provided', () => {
const md = generateMarkdown({ ...baseRest, feel: 'N' });
expect(md).toContain('F: N');
});

it('omits feel when not set', () => {
const md = generateMarkdown(baseRest);
expect(md).not.toContain('F:');
});

it('omits empty optional fields', () => {
const md = generateMarkdown(baseRest);
expect(md).not.toContain('HRV:');
Expand Down
20 changes: 0 additions & 20 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading