Skip to content
Merged
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
5 changes: 3 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,17 +117,18 @@ Old drafts (pre-entryType) are migrated to `cycling` on restore in `use-form-per

### Workout Schema Fields
Always required: `entryType` (`cycling`/`rest`/`other`), `workoutDate`
Shared-optional: `dailyNotes` (free-form, rendered as bullets; visible on all entry types below the Entry Type selector)
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). Rest entries also reuse `hrv`/`rMSSD`/`rhr`/`trainerRoadLgt`.
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 and `Weight` + bulleted `dailyNotes`
- **other**: optional `G: <activity>` + bulleted `activityNotes`; no metrics

## Environment Variables (Deployment Only)
Expand Down
6 changes: 6 additions & 0 deletions client/src/hooks/use-form-persistence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ export function useFormPersistence<T extends FieldValues>(
if (data && typeof data === "object" && !data.entryType) {
data.entryType = "cycling";
}
if (data && typeof data === "object" && data.restNotes && !data.dailyNotes) {
data.dailyNotes = data.restNotes;
}
if (data && typeof data === "object") {
delete data.restNotes;
}
form.reset(draft.data);
setWasRestored(true);
}
Expand Down
69 changes: 39 additions & 30 deletions client/src/pages/home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,6 @@ const FIELD_TO_SECTION: Partial<Record<keyof InsertWorkout, SectionId>> = {
whatCouldBeImproved: "reflection",
description: "reflection",
weight: "rest-day",
restNotes: "rest-day",
activityGoal: "activity",
activityNotes: "activity",
};
Expand Down Expand Up @@ -149,8 +148,8 @@ function getDefaultWorkoutValues(): InsertWorkout {
whatWentWell: "",
whatCouldBeImproved: "",
description: "",
dailyNotes: "",
weight: undefined,
restNotes: "",
activityGoal: "",
activityNotes: "",
};
Expand Down Expand Up @@ -228,6 +227,10 @@ function generateCyclingMarkdown(data: InsertWorkout): string {
markdown += data.description + '\n';
}

if (data.dailyNotes) {
markdown += '\n' + formatBulletPoints(data.dailyNotes) + '\n';
}

return markdown;
}

Expand All @@ -242,8 +245,8 @@ function generateRestMarkdown(data: InsertWorkout): string {
}
if (data.weight) markdown += `Weight: ${data.weight}\n`;

if (data.restNotes) {
markdown += '\n' + formatBulletPoints(data.restNotes) + '\n';
if (data.dailyNotes) {
markdown += '\n' + formatBulletPoints(data.dailyNotes) + '\n';
}

return markdown;
Expand All @@ -258,6 +261,10 @@ function generateOtherMarkdown(data: InsertWorkout): string {
markdown += '\n' + formatBulletPoints(data.activityNotes) + '\n';
}

if (data.dailyNotes) {
markdown += '\n' + formatBulletPoints(data.dailyNotes) + '\n';
}

return markdown;
}

Expand Down Expand Up @@ -724,6 +731,33 @@ export default function Home() {
</div>
</div>

{/* Daily Notes — all entry types */}
<div className="bg-gray-50 dark:bg-gray-700 rounded-lg p-4">
<FormField
control={form.control}
name="dailyNotes"
render={({ field }) => (
<FormItem>
<FormLabel className="flex items-center gap-2">
<NotebookPen className="w-4 h-4 text-blue-600 dark:text-blue-400" />
Daily Notes
</FormLabel>
<FormControl>
<Textarea
rows={4}
placeholder="Life context, mood, decisions, stressors... One thought per line."
className="resize-y"
{...field}
value={field.value ?? ""}
/>
</FormControl>
<p className="text-xs text-gray-500">Each line will be formatted as a bullet point</p>
<FormMessage />
</FormItem>
)}
/>
</div>

{/* Core Metrics Section — Cycling only */}
{entryType === "cycling" && (
<CollapsibleSection
Expand Down Expand Up @@ -1269,9 +1303,7 @@ export default function Home() {
title="Rest Day"
isOpen={sectionStates["rest-day"]}
onOpenChange={(open) => setSection("rest-day", open)}
hasData={
watchedValues.weight !== undefined || !!watchedValues.restNotes
}
hasData={watchedValues.weight !== undefined}
>
<FormField
control={form.control}
Expand All @@ -1297,29 +1329,6 @@ export default function Home() {
)}
/>

<FormField
control={form.control}
name="restNotes"
render={({ field }) => (
<FormItem>
<FormLabel className="flex items-center gap-2">
<NotebookPen className="w-4 h-4 text-blue-500" />
Notes
</FormLabel>
<FormControl>
<Textarea
rows={6}
placeholder="How are you feeling? Life context, decisions, mood..."
className="resize-y"
{...field}
value={field.value ?? ""}
/>
</FormControl>
<p className="text-xs text-gray-500">Each line will be formatted as a bullet point</p>
<FormMessage />
</FormItem>
)}
/>
</CollapsibleSection>
)}

Expand Down
2 changes: 1 addition & 1 deletion client/src/test/schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ describe('Workout Schema Validation', () => {
rMSSD: 30,
rhr: 63,
weight: 82.5,
restNotes: 'Took a real day off',
dailyNotes: 'Took a real day off',
};
const result = insertWorkoutSchema.safeParse(restWorkout);
expect(result.success).toBe(true);
Expand Down
74 changes: 70 additions & 4 deletions client/src/test/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,10 @@ function generateCyclingMarkdown(data: InsertWorkout): string {
markdown += data.description + '\n';
}

if (data.dailyNotes) {
markdown += '\n' + formatBulletPoints(data.dailyNotes) + '\n';
}

return markdown;
}

Expand All @@ -133,8 +137,8 @@ function generateRestMarkdown(data: InsertWorkout): string {
}
if (data.weight) markdown += `Weight: ${data.weight}\n`;

if (data.restNotes) {
markdown += '\n' + formatBulletPoints(data.restNotes) + '\n';
if (data.dailyNotes) {
markdown += '\n' + formatBulletPoints(data.dailyNotes) + '\n';
}

return markdown;
Expand All @@ -149,6 +153,10 @@ function generateOtherMarkdown(data: InsertWorkout): string {
markdown += '\n' + formatBulletPoints(data.activityNotes) + '\n';
}

if (data.dailyNotes) {
markdown += '\n' + formatBulletPoints(data.dailyNotes) + '\n';
}

return markdown;
}

Expand Down Expand Up @@ -235,10 +243,10 @@ describe('generateMarkdown — rest output', () => {
expect(md).not.toContain('TR-LGT');
});

it('formats rest notes as bullet points', () => {
it('formats daily notes as bullet points on rest entry', () => {
const md = generateMarkdown({
...baseRest,
restNotes: 'Yesterday was stressful\nDecided to skip training',
dailyNotes: 'Yesterday was stressful\nDecided to skip training',
});
expect(md).toContain('- Yesterday was stressful');
expect(md).toContain('- Decided to skip training');
Expand Down Expand Up @@ -324,4 +332,62 @@ describe('generateMarkdown — other output', () => {
expect(md).not.toContain('NP:');
expect(md).not.toContain('TSS:');
});

it('includes daily notes as bullets after activity notes', () => {
const md = generateMarkdown({
...baseOther,
activityGoal: 'MFR',
activityNotes: 'Full body protocol v3',
dailyNotes: 'Long day at work\nApartment move stress',
});
expect(md).toContain('- Full body protocol v3');
expect(md).toContain('- Long day at work');
expect(md).toContain('- Apartment move stress');
const activityIdx = md.indexOf('- Full body protocol v3');
const dailyIdx = md.indexOf('- Long day at work');
expect(activityIdx).toBeLessThan(dailyIdx);
});

it('includes daily notes when no activity notes', () => {
const md = generateMarkdown({
...baseOther,
dailyNotes: 'Rest context',
});
expect(md).toContain('- Rest context');
});
});

describe('generateMarkdown — cycling daily notes', () => {
const base: InsertWorkout = {
entryType: 'cycling',
workoutDate: '2026-04-13',
goal: 'Threshold',
rpe: 7,
feel: 'G',
};

it('includes daily notes at end of cycling output', () => {
const md = generateMarkdown({
...base,
dailyNotes: 'New apartment adaptation\nBarometric pressure dropped',
});
expect(md).toContain('- New apartment adaptation');
expect(md).toContain('- Barometric pressure dropped');
});

it('daily notes appear after Planned when both present', () => {
const md = generateMarkdown({
...base,
description: 'Sweet Spot Base II',
dailyNotes: 'Commute stress',
});
const plannedIdx = md.indexOf('Planned');
const dailyIdx = md.indexOf('- Commute stress');
expect(plannedIdx).toBeLessThan(dailyIdx);
});

it('omits daily notes section when empty', () => {
const md = generateMarkdown(base);
expect(md).not.toContain('- ');
});
});
Loading
Loading