screenshots/12Jan2026/implementation_plan.md
Rama main · réplica de lectura del repositorio original.
# Implementation Plan: January 12, 2026 Features
Based on the screenshots and explanations, this document outlines the implementation plan for 5 new features in ActaLog.
---
## Feature 1: RPE (Rate of Perceived Exertion) Tracking
**Priority:** HIGH
**Complexity:** MEDIUM
**Files to modify:** ~12 files
### Overview
Add RPE (2-10 scale) to workout logging for both movements and WODs.
### Visual Reference
Based on RPE-scale_large.webp, the scale uses color coding:
- **10 (Red):** Max effort - Almost impossible to keep going, completely out of breath
- **9 (Red-Orange):** Very hard - Very difficult to maintain, barely breathe/speak
- **7-8 (Orange):** Vigorous - Borderline uncomfortable, short of breath
- **4-6 (Yellow):** Moderate - Breathing heavily, can hold short conversation
- **2-3 (Green):** Light - Can maintain for hours, easy to breathe and talk
### Implementation Steps
#### 1.1 Database Migration
Create migration: `migrations/XXXXXX_add_rpe_columns.up.sql`
```sql
ALTER TABLE user_workout_movements ADD COLUMN rpe INTEGER;
ALTER TABLE user_workout_wods ADD COLUMN rpe INTEGER;
```
#### 1.2 Domain Layer Changes
**File:** `internal/domain/movement.go`
- Add `RPE *int` field to `UserWorkoutMovement` struct (after `Notes` field)
**File:** `internal/domain/wod.go`
- Add `RPE *int` field to `UserWorkoutWOD` struct (after `Notes` field)
#### 1.3 Repository Layer Changes
**File:** `internal/repository/user_workout_movement_repository.go`
- Update INSERT queries to include `rpe` column
- Update SELECT queries to retrieve `rpe`
- Update UPDATE queries to modify `rpe`
**File:** `internal/repository/user_workout_wod_repository.go`
- Same updates as movement repository
**File:** `internal/repository/database.go`
- Update CREATE TABLE statements for `user_workout_movements` and `user_workout_wods`
#### 1.4 Handler Layer Changes
**File:** `internal/handler/user_workout_handler.go`
- Add `RPE *int` to `MovementPerformance` struct
- Add `RPE *int` to `WODPerformance` struct
- Update request parsing and response building
#### 1.5 Frontend Changes
**File:** `web/src/views/LogWorkoutView.vue`
- **Movement Performance section (lines 137-222):** Add RPE v-select after notes textarea (line 220)
- **WOD Performance section (lines 229-350):** Add RPE v-select after notes textarea (line 348)
- Add `rpe` field to `movementPerformance` and `wodPerformance` arrays in script section
**File:** `web/src/views/PerformanceView.vue`
- Display RPE in Performance History entries (lines 282-325)
- Show RPE badge/chip next to date if recorded
**File:** `web/src/components/rpe-scale.js` (NEW - shared utility)
- Export RPE_SCALE constant for reuse across components
#### 1.6 RPE Scale Reference Data (Color-Coded)
Create static reference in frontend matching the visual reference:
```javascript
const RPE_SCALE = [
{ value: null, text: 'Not recorded', color: 'grey' },
{ value: 2, text: '2 - Very light', description: 'Like a slow walk', color: '#4CAF50' },
{ value: 3, text: '3 - Light', description: 'Comfortable, easy conversation', color: '#8BC34A' },
{ value: 4, text: '4 - Light-Moderate', description: 'Comfortable pace', color: '#CDDC39' },
{ value: 5, text: '5 - Moderate', description: 'Breathing heavier, can talk', color: '#FFEB3B' },
{ value: 6, text: '6 - Moderate-Hard', description: 'More challenging', color: '#FFC107' },
{ value: 7, text: '7 - Vigorous', description: 'Uncomfortable, short sentences', color: '#FF9800' },
{ value: 8, text: '8 - Hard', description: 'Heavy breathing, few words', color: '#FF5722' },
{ value: 9, text: '9 - Very hard', description: 'Can barely speak', color: '#F44336' },
{ value: 10, text: '10 - Max effort', description: 'All-out, cannot continue', color: '#D32F2F' }
]
```
---
## Feature 2: Detail View on Card Click
**Priority:** MEDIUM
**Complexity:** LOW-MEDIUM
**Files to modify:** ~6-8 files
### Overview
Clicking any WOD, Template, or Movement card should display a read-only detail view with Back button, QuickLog link, and Edit button.
### Implementation Steps
#### 2.1 Create Reusable Detail Dialog Component
**New file:** `web/src/components/DetailViewDialog.vue`
- Props: `type` (wod|template|movement), `id`, `show`
- Emits: `close`, `quicklog`, `edit`
- Fetch data based on type and id
- Render markdown description
- Include action buttons: Back, QuickLog, Edit
#### 2.2 Update Card Components
Identify all locations where cards are rendered:
- `web/src/views/LogWorkoutView.vue` - Template/WOD selection cards
- `web/src/views/MovementListView.vue` - Movement list
- `web/src/views/WODListView.vue` - WOD list
- `web/src/views/WorkoutTemplateListView.vue` - Template list
- `web/src/views/DashboardView.vue` - Recent activity cards
For each, add:
- `@click` handler on card body (not on existing action buttons)
- Check if click target is an action button; if not, open detail dialog
- Pass entity type and ID to dialog
#### 2.3 Dialog Content Structure
```vue
<v-dialog v-model="showDialog" max-width="600">
<v-card>
<v-card-title>{{ entityName }}</v-card-title>
<v-card-text>
<!-- Rendered markdown description -->
<!-- Key details (type, score type, etc.) -->
</v-card-text>
<v-card-actions>
<v-btn @click="close">Back</v-btn>
<v-spacer />
<v-btn color="warning" @click="quicklog">QuickLog</v-btn>
<v-btn color="primary" @click="edit">Edit</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
```
---
## Feature 3: Fix Workout Summary Stats
**Priority:** HIGH
**Complexity:** LOW
**Files to modify:** ~2-3 files
### Overview
Verify and fix workout summary statistics on User Profile/Dashboard screen, particularly "All Time" calculations.
### Investigation Steps
#### 3.1 Review Current Implementation
**File:** `web/src/views/DashboardView.vue`
- Examine how stats are calculated/fetched
- Check if "All Time" uses correct date range (no bounds)
**File:** `internal/handler/user_workout_handler.go`
- Review endpoints that provide stats data
- Check query parameters for date filtering
**File:** `internal/repository/user_workout_repository.go`
- Review `ListByUserAndDateRange()` function
- Check if null date bounds are handled correctly
#### 3.2 Potential Issues to Check
1. Date range filters incorrectly applied to "All Time"
2. Off-by-one errors in month/week calculations
3. Timezone handling issues
4. Cached values not updating
#### 3.3 Fix Implementation
Based on investigation findings:
- Update repository queries if date filtering is wrong
- Update frontend stat calculation if client-side issue
- Add tests for edge cases
---
## Feature 4: Percent of Best Section
**Priority:** MEDIUM
**Complexity:** MEDIUM
**Files to modify:** ~1-2 files
### Overview
Add "Percent of Best" section to Movement Stats screen showing weight percentages based on heaviest lift.
### Current PerformanceView.vue Structure (Movement-specific, lines 78-189)
Current order:
1. Heaviest Lifts (lines 79-119) - v-card, NOT collapsible
2. Best Estimated 1RM (lines 121-153) - v-card, NOT collapsible
3. Rep Scheme Dropdown Filter (lines 155-171)
4. Performance Chart (lines 173-188) - v-card, NOT collapsible
**Required Changes:**
1. Wrap all sections in `v-expansion-panels`
2. Reorder to: Best Estimated 1RM → Percent of Best (NEW) → Heaviest Lifts → Performance Chart
3. Default expanded: Heaviest Lifts, Percent of Best
4. Remove Rep Scheme Dropdown (or move inside Performance Chart panel)
### Implementation Steps
#### 4.1 Frontend Component Changes
**File:** `web/src/views/PerformanceView.vue`
Replace lines 79-189 with collapsible expansion panels:
```vue
<!-- MOVEMENT-SPECIFIC CONTENT -->
<template v-if="selectedItem.type === 'movement'">
<v-expansion-panels v-model="expandedPanels" multiple class="mb-3">
<!-- 1. Best Estimated 1RM -->
<v-expansion-panel value="best1rm">
<v-expansion-panel-title>
<v-icon color="warning" size="small" class="mr-2">mdi-arm-flex</v-icon>
Best Estimated 1RM
</v-expansion-panel-title>
<v-expansion-panel-text>
<!-- existing Best 1RM content from lines 128-152 -->
</v-expansion-panel-text>
</v-expansion-panel>
<!-- 2. Percent of Best (NEW) -->
<v-expansion-panel v-if="showPercentOfBest" value="percentOfBest">
<v-expansion-panel-title>
<v-icon color="primary" size="small" class="mr-2">mdi-percent</v-icon>
Percent of Best
</v-expansion-panel-title>
<v-expansion-panel-text>
<v-table density="compact">
<thead>
<tr>
<th class="text-left">% of Heaviest</th>
<th class="text-right">{{ weightUnit }}</th>
</tr>
</thead>
<tbody>
<tr v-for="pct in percentages" :key="pct.percent">
<td>{{ pct.percent }}%</td>
<td class="text-right font-weight-bold">{{ pct.weight }}</td>
</tr>
</tbody>
</v-table>
</v-expansion-panel-text>
</v-expansion-panel>
<!-- 3. Heaviest Lifts -->
<v-expansion-panel value="heaviestLifts">
<v-expansion-panel-title>
<v-icon color="primary" size="small" class="mr-2">mdi-trophy</v-icon>
Heaviest Lifts
</v-expansion-panel-title>
<v-expansion-panel-text>
<!-- existing Heaviest Lifts content from lines 86-117 -->
</v-expansion-panel-text>
</v-expansion-panel>
<!-- 4. Performance Chart -->
<v-expansion-panel value="performanceChart">
<v-expansion-panel-title>
<v-icon color="primary" size="small" class="mr-2">mdi-chart-line</v-icon>
Performance Chart
</v-expansion-panel-title>
<v-expansion-panel-text>
<!-- Rep Scheme Filter + Chart from lines 156-187 -->
</v-expansion-panel-text>
</v-expansion-panel>
</v-expansion-panels>
</template>
```
#### 4.2 Add Script Section Variables
```javascript
// Default expanded panels (Heaviest Lifts and Percent of Best)
const expandedPanels = ref(['heaviestLifts', 'percentOfBest'])
// User's weight unit from settings
const weightUnit = computed(() => settingsStore.weightUnit || 'lbs')
// Get heaviest weight from data
const heaviestWeight = computed(() => {
if (heaviestLifts.value.length === 0) return null
return heaviestLifts.value[0].weight
})
// Show Percent of Best only for movements with weight data
const showPercentOfBest = computed(() => {
return heaviestWeight.value && heaviestWeight.value > 0
})
// Calculate percentages based on heaviest lift
const percentages = computed(() => {
if (!heaviestWeight.value) return []
// Percentages as specified: 95%, 90%, 85%, 80%, 75%, 70%, 65%, 60%, 55%, 50%, 25%
const percents = [95, 90, 85, 80, 75, 70, 65, 60, 55, 50, 25]
return percents.map(pct => ({
percent: pct,
weight: Math.round(heaviestWeight.value * (pct / 100))
}))
})
```
#### 4.3 Visual Design
- Two-column grid layout as shown in image_005.jpg
- Column 1: "% of Heaviest" (left-aligned)
- Column 2: Weight value in user's units (right-aligned, bold)
- Clean, compact table styling
---
## Feature 5: Calendar Date Click Detail View
**Priority:** MEDIUM
**Complexity:** MEDIUM
**Files to modify:** ~3-4 files
### Overview
Clicking a date in the calendar should show detailed view of all workouts logged on that date.
### Implementation Steps
#### 5.1 Update Calendar Component
**File:** `web/src/components/WorkoutCalendar.vue`
- Already emits `daySelected` event with Date object
- No changes needed here
#### 5.2 Create Date Workout Detail Component
**New file:** `web/src/components/DateWorkoutDetail.vue`
- Props: `date` (Date object), `show` (boolean)
- Emits: `close`, `quicklog`, `edit`
- Fetches all workouts for the selected date
- Displays each workout with:
- Template/WOD name
- Movements performed
- Scores/times
- Back button, QuickLog link, Edit button
#### 5.3 API Endpoint (if not exists)
Check if endpoint exists for fetching workouts by date:
- `GET /api/user-workouts?date=YYYY-MM-DD`
If not, add to:
**File:** `internal/handler/user_workout_handler.go`
- Add date query parameter filtering
- Return workouts with full details (movements, WODs)
**File:** `internal/repository/user_workout_repository.go`
- Add `GetByUserIDAndDate(userID, date)` method if not exists
#### 5.4 Update Views Using Calendar
**File:** `web/src/views/DashboardView.vue`
- Handle `@daySelected` event
- Open DateWorkoutDetail dialog
**File:** `web/src/views/WorkoutCalendarView.vue`
- Handle `@daySelected` event
- Open DateWorkoutDetail dialog
#### 5.5 Dialog Structure
```vue
<v-dialog v-model="showDateDetail" max-width="600">
<v-card>
<v-card-title>
{{ formatDate(selectedDate) }}
</v-card-title>
<v-card-text>
<div v-if="workouts.length === 0">
No workouts logged on this date.
</div>
<v-list v-else>
<v-list-item v-for="workout in workouts" :key="workout.id">
<v-list-item-title>{{ workout.name || 'Workout' }}</v-list-item-title>
<v-list-item-subtitle>
{{ workout.movements?.length || 0 }} movements,
{{ workout.wods?.length || 0 }} WODs
</v-list-item-subtitle>
<template #append>
<v-btn icon size="small" @click="editWorkout(workout)">
<v-icon>mdi-pencil</v-icon>
</v-btn>
</template>
</v-list-item>
</v-list>
</v-card-text>
<v-card-actions>
<v-btn @click="close">Back</v-btn>
<v-spacer />
<v-btn color="warning" @click="quicklogDate">QuickLog</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
```
---
## Implementation Order (Recommended)
1. **Feature 3: Fix Workout Summary Stats** - Quick win, investigate and fix
2. **Feature 1: RPE Tracking** - Foundational data model change, do early
3. **Feature 4: Percent of Best** - Frontend-only, can be done in parallel
4. **Feature 2: Detail View on Card Click** - Reusable component, moderate effort
5. **Feature 5: Calendar Date Click** - Builds on Feature 2's patterns
---
## Testing Checklist
### RPE Feature
- [ ] RPE saves correctly for movements
- [ ] RPE saves correctly for WODs
- [ ] RPE displays in workout history
- [ ] RPE is optional (null allowed)
- [ ] RPE validates range (2-10)
### Detail View Feature
- [ ] WOD detail opens on card click
- [ ] Template detail opens on card click
- [ ] Movement detail opens on card click
- [ ] Back button closes dialog
- [ ] QuickLog navigates correctly
- [ ] Edit navigates correctly
- [ ] Existing card buttons still work
### Stats Fix
- [ ] All Time shows correct total
- [ ] This Month shows correct count
- [ ] This Week shows correct count
- [ ] Streak calculates correctly
### Percent of Best
- [ ] Shows for strength movements
- [ ] Hidden for non-weight movements
- [ ] Calculations are correct
- [ ] Section is collapsible
- [ ] Default expanded state correct
- [ ] Section order correct
### Calendar Date Click
- [ ] Click opens detail view
- [ ] Shows all workouts for date
- [ ] Empty state for no workouts
- [ ] Back button works
- [ ] Edit button navigates correctly
---
## Migration Notes
### Database
- Migrations should be backwards compatible
- New columns should be nullable
- No data migration needed (new fields only)
### API
- New fields should be optional in requests
- Existing clients should continue working
- Version bump: 0.22.0 -> 0.23.0 (feature release)
---
## File Reference Summary
| Feature | Backend Files | Frontend Files |
|---------|---------------|----------------|
| **1. RPE** | `domain/movement.go`, `domain/wod.go`, `repository/user_workout_movement_repository.go`, `repository/user_workout_wod_repository.go`, `repository/database.go`, `handler/user_workout_handler.go` | `views/LogWorkoutView.vue`, `views/PerformanceView.vue`, `components/rpe-scale.js` (new) |
| **2. Detail View** | None | `components/DetailViewDialog.vue` (new), `views/LogWorkoutView.vue`, various list views |
| **3. Stats Fix** | `handler/user_workout_handler.go`, `repository/user_workout_repository.go` | `views/DashboardView.vue` |
| **4. Percent of Best** | None | `views/PerformanceView.vue` |
| **5. Calendar Click** | `handler/user_workout_handler.go` (if date filter needed) | `components/DateWorkoutDetail.vue` (new), `views/DashboardView.vue`, `views/WorkoutCalendarView.vue` |
---
## Quick Start Commands
```bash
# Create RPE migration
make migrate-create name=add_rpe_columns
# Run backend tests
make test
# Run frontend dev server
cd web && npm run dev
# Run frontend tests
cd web && npm run test:run
```
---
## Notes
1. **RPE Percentages Note:** The original spec listed "75%, 60%, 65%" which appears to be a typo. Implementation assumes descending order: 95%, 90%, 85%, 80%, 75%, 70%, 65%, 60%, 55%, 50%, 25%.
2. **Detail View Component:** Consider making a single reusable `DetailViewDialog.vue` component that accepts entity type (wod/template/movement) and ID, rather than three separate components.
3. **Calendar Integration:** The existing `WorkoutCalendar.vue` already emits `daySelected`. Only need to add handler in parent views.
4. **Stats Fix Priority:** This should be investigated first as it may reveal calculation bugs that affect other features.