mirror of
https://github.com/sstent/AICyclingCoach.git
synced 2026-01-25 08:34:51 +00:00
sync - tui loads but no data in views
This commit is contained in:
@@ -1,261 +0,0 @@
|
||||
# 🎯 **Backend Implementation TODO List**
|
||||
|
||||
## **Priority 1: Core API Gaps (Essential)**
|
||||
|
||||
### **1.1 Plan Generation Endpoint**
|
||||
- [ ] **Add plan generation endpoint** in `app/routes/plan.py`
|
||||
```python
|
||||
@router.post("/generate", response_model=PlanSchema)
|
||||
async def generate_plan(
|
||||
plan_request: PlanGenerationRequest,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
```
|
||||
- [ ] **Create PlanGenerationRequest schema** in `app/schemas/plan.py`
|
||||
```python
|
||||
class PlanGenerationRequest(BaseModel):
|
||||
rule_ids: List[UUID]
|
||||
goals: Dict[str, Any]
|
||||
user_preferences: Optional[Dict[str, Any]] = None
|
||||
duration_weeks: int = 12
|
||||
```
|
||||
- [ ] **Update AIService.generate_plan()** to handle rule fetching from DB
|
||||
- [ ] **Add validation** for rule compatibility and goal requirements
|
||||
- [ ] **Add tests** for plan generation workflow
|
||||
|
||||
### **1.2 Rule Parsing API**
|
||||
- [ ] **Add natural language rule parsing endpoint** in `app/routes/rule.py`
|
||||
```python
|
||||
@router.post("/parse-natural-language")
|
||||
async def parse_natural_language_rules(
|
||||
request: NaturalLanguageRuleRequest,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
```
|
||||
- [ ] **Create request/response schemas** in `app/schemas/rule.py`
|
||||
```python
|
||||
class NaturalLanguageRuleRequest(BaseModel):
|
||||
natural_language_text: str
|
||||
rule_name: str
|
||||
|
||||
class ParsedRuleResponse(BaseModel):
|
||||
parsed_rules: Dict[str, Any]
|
||||
confidence_score: Optional[float]
|
||||
suggestions: Optional[List[str]]
|
||||
```
|
||||
- [ ] **Enhance AIService.parse_rules_from_natural_language()** with better error handling
|
||||
- [ ] **Add rule validation** after parsing
|
||||
- [ ] **Add preview mode** before saving parsed rules
|
||||
|
||||
### **1.3 Section Integration with GPX Parsing**
|
||||
- [ ] **Update `app/services/gpx.py`** to create sections automatically
|
||||
```python
|
||||
async def parse_gpx_with_sections(file_path: str, route_id: UUID, db: AsyncSession) -> dict:
|
||||
# Parse GPX into segments
|
||||
# Create Section records for each segment
|
||||
# Return enhanced GPX data with section metadata
|
||||
```
|
||||
- [ ] **Modify `app/routes/gpx.py`** to create sections after route creation
|
||||
- [ ] **Add section creation logic** in GPX upload workflow
|
||||
- [ ] **Update Section model** to include more GPX-derived metadata
|
||||
- [ ] **Add section querying endpoints** for route visualization
|
||||
|
||||
## **Priority 2: Data Model Enhancements**
|
||||
|
||||
### **2.1 Missing Schema Fields**
|
||||
- [ ] **Add missing fields to User model** in `app/models/user.py`
|
||||
```python
|
||||
class User(BaseModel):
|
||||
name: Optional[str]
|
||||
email: Optional[str]
|
||||
fitness_level: Optional[str]
|
||||
preferences: Optional[JSON]
|
||||
```
|
||||
- [ ] **Enhance Plan model** with additional metadata
|
||||
```python
|
||||
class Plan(BaseModel):
|
||||
user_id: Optional[UUID] = Column(ForeignKey("users.id"))
|
||||
name: str
|
||||
description: Optional[str]
|
||||
start_date: Optional[Date]
|
||||
end_date: Optional[Date]
|
||||
goal_type: Optional[str]
|
||||
active: Boolean = Column(default=True)
|
||||
```
|
||||
- [ ] **Add plan-rule relationship table** (already exists but ensure proper usage)
|
||||
- [ ] **Update all schemas** to match enhanced models
|
||||
|
||||
### **2.2 Database Relationships**
|
||||
- [ ] **Fix User-Plan relationship** in models
|
||||
- [ ] **Add cascade delete rules** where appropriate
|
||||
- [ ] **Add database constraints** for data integrity
|
||||
- [ ] **Create missing indexes** for performance
|
||||
```sql
|
||||
CREATE INDEX idx_workouts_garmin_activity_id ON workouts(garmin_activity_id);
|
||||
CREATE INDEX idx_plans_user_active ON plans(user_id, active);
|
||||
CREATE INDEX idx_analyses_workout_approved ON analyses(workout_id, approved);
|
||||
```
|
||||
|
||||
## **Priority 3: API Completeness**
|
||||
|
||||
### **3.1 Export/Import Functionality**
|
||||
- [ ] **Create export service** `app/services/export_import.py`
|
||||
```python
|
||||
class ExportImportService:
|
||||
async def export_user_data(user_id: UUID) -> bytes:
|
||||
async def export_routes() -> bytes:
|
||||
async def import_user_data(data: bytes, user_id: UUID):
|
||||
```
|
||||
- [ ] **Add export endpoints** in new `app/routes/export.py`
|
||||
```python
|
||||
@router.get("/export/routes")
|
||||
@router.get("/export/plans/{plan_id}")
|
||||
@router.get("/export/user-data")
|
||||
@router.post("/import/routes")
|
||||
@router.post("/import/plans")
|
||||
```
|
||||
- [ ] **Support multiple formats** (JSON, GPX, ZIP)
|
||||
- [ ] **Add data validation** for imports
|
||||
- [ ] **Handle version compatibility** for imports
|
||||
|
||||
### **3.2 Enhanced Dashboard API**
|
||||
- [ ] **Expand dashboard data** in `app/routes/dashboard.py`
|
||||
```python
|
||||
@router.get("/metrics/weekly")
|
||||
@router.get("/metrics/monthly")
|
||||
@router.get("/progress/{plan_id}")
|
||||
@router.get("/upcoming-workouts")
|
||||
```
|
||||
- [ ] **Add aggregation queries** for metrics
|
||||
- [ ] **Cache dashboard data** for performance
|
||||
- [ ] **Add real-time updates** capability
|
||||
|
||||
### **3.3 Advanced Workout Features**
|
||||
- [ ] **Add workout comparison endpoint**
|
||||
```python
|
||||
@router.get("/workouts/{workout_id}/compare/{compare_workout_id}")
|
||||
```
|
||||
- [ ] **Add workout search/filtering**
|
||||
```python
|
||||
@router.get("/workouts/search")
|
||||
async def search_workouts(
|
||||
activity_type: Optional[str] = None,
|
||||
date_range: Optional[DateRange] = None,
|
||||
power_range: Optional[PowerRange] = None
|
||||
):
|
||||
```
|
||||
- [ ] **Add bulk workout operations**
|
||||
- [ ] **Add workout tagging system**
|
||||
|
||||
## **Priority 4: Service Layer Improvements**
|
||||
|
||||
### **4.1 AI Service Enhancements**
|
||||
- [ ] **Add prompt caching** to reduce API calls
|
||||
- [ ] **Implement prompt A/B testing** framework
|
||||
- [ ] **Add AI response validation** and confidence scoring
|
||||
- [ ] **Create AI service health checks**
|
||||
- [ ] **Add fallback mechanisms** for AI failures
|
||||
- [ ] **Implement rate limiting** for AI calls
|
||||
- [ ] **Add cost tracking** for AI API usage
|
||||
|
||||
### **4.2 Garmin Service Improvements**
|
||||
- [ ] **Add incremental sync** instead of full sync
|
||||
- [ ] **Implement activity deduplication** logic
|
||||
- [ ] **Add webhook support** for real-time sync
|
||||
- [ ] **Enhance error recovery** for failed syncs
|
||||
- [ ] **Add activity type filtering**
|
||||
- [ ] **Support multiple Garmin accounts** per user
|
||||
|
||||
### **4.3 Plan Evolution Enhancements**
|
||||
- [ ] **Add plan comparison** functionality
|
||||
- [ ] **Implement plan rollback** mechanism
|
||||
- [ ] **Add plan branching** for different scenarios
|
||||
- [ ] **Create plan templates** system
|
||||
- [ ] **Add automated plan adjustments** based on performance
|
||||
|
||||
## **Priority 5: Validation & Error Handling**
|
||||
|
||||
### **5.1 Input Validation**
|
||||
- [ ] **Add comprehensive Pydantic validators** for all schemas
|
||||
- [ ] **Validate GPX file integrity** before processing
|
||||
- [ ] **Add business rule validation** (e.g., plan dates, workout conflicts)
|
||||
- [ ] **Validate AI responses** before storing
|
||||
- [ ] **Add file size/type restrictions**
|
||||
|
||||
### **5.2 Error Handling**
|
||||
- [ ] **Create custom exception hierarchy**
|
||||
```python
|
||||
class CyclingCoachException(Exception):
|
||||
class GarminSyncError(CyclingCoachException):
|
||||
class AIServiceError(CyclingCoachException):
|
||||
class PlanGenerationError(CyclingCoachException):
|
||||
```
|
||||
- [ ] **Add global exception handler**
|
||||
- [ ] **Improve error messages** for user feedback
|
||||
- [ ] **Add error recovery mechanisms**
|
||||
- [ ] **Log errors with context** for debugging
|
||||
|
||||
## **Priority 6: Performance & Monitoring**
|
||||
|
||||
### **6.1 Performance Optimizations**
|
||||
- [ ] **Add database query optimization**
|
||||
- [ ] **Implement caching** for frequently accessed data
|
||||
- [ ] **Add connection pooling** configuration
|
||||
- [ ] **Optimize GPX file parsing** for large files
|
||||
- [ ] **Add pagination** to list endpoints
|
||||
- [ ] **Implement background job queue** for long-running tasks
|
||||
|
||||
### **6.2 Enhanced Monitoring**
|
||||
- [ ] **Add application metrics** (response times, error rates)
|
||||
- [ ] **Create health check dependencies**
|
||||
- [ ] **Add performance profiling** endpoints
|
||||
- [ ] **Implement alerting** for critical errors
|
||||
- [ ] **Add audit logging** for data changes
|
||||
|
||||
## **Priority 7: Security & Configuration**
|
||||
|
||||
### **7.1 Security Improvements**
|
||||
- [ ] **Implement user authentication/authorization**
|
||||
- [ ] **Add rate limiting** to prevent abuse
|
||||
- [ ] **Validate file uploads** for security
|
||||
- [ ] **Add CORS configuration** properly
|
||||
- [ ] **Implement request/response logging** (without sensitive data)
|
||||
- [ ] **Add API versioning** support
|
||||
|
||||
### **7.2 Configuration Management**
|
||||
- [ ] **Add environment-specific configs**
|
||||
- [ ] **Validate configuration** on startup
|
||||
- [ ] **Add feature flags** system
|
||||
- [ ] **Implement secrets management**
|
||||
- [ ] **Add configuration reload** without restart
|
||||
|
||||
## **Priority 8: Testing & Documentation**
|
||||
|
||||
### **8.1 Testing**
|
||||
- [ ] **Create comprehensive test suite**
|
||||
- Unit tests for services
|
||||
- Integration tests for API endpoints
|
||||
- Database migration tests
|
||||
- AI service mock tests
|
||||
- [ ] **Add test fixtures** for common data
|
||||
- [ ] **Implement test database** setup/teardown
|
||||
- [ ] **Add performance tests** for critical paths
|
||||
- [ ] **Create end-to-end tests** for workflows
|
||||
|
||||
### **8.2 Documentation**
|
||||
- [ ] **Generate OpenAPI documentation**
|
||||
- [ ] **Add endpoint documentation** with examples
|
||||
- [ ] **Create service documentation**
|
||||
- [ ] **Document deployment procedures**
|
||||
- [ ] **Add troubleshooting guides**
|
||||
|
||||
---
|
||||
|
||||
## **🎯 Recommended Implementation Order:**
|
||||
|
||||
1. **Week 1:** Priority 1 (Core API gaps) - Essential for feature completeness
|
||||
2. **Week 2:** Priority 2 (Data model) + Priority 5.1 (Validation) - Foundation improvements
|
||||
3. **Week 3:** Priority 3.1 (Export/Import) + Priority 4.1 (AI improvements) - User-facing features
|
||||
4. **Week 4:** Priority 6 (Performance) + Priority 8.1 (Testing) - Production readiness
|
||||
|
||||
This todo list will bring your backend implementation to 100% design doc compliance and beyond, making it production-ready with enterprise-level features! 🚀
|
||||
@@ -1,255 +0,0 @@
|
||||
# Frontend Development TODO List
|
||||
|
||||
## 🚨 Critical Missing Features (High Priority)
|
||||
|
||||
### 1. Rules Management System
|
||||
- [ ] **Create Rules page component** (`/src/pages/Rules.jsx`)
|
||||
- [ ] Natural language textarea editor
|
||||
- [ ] AI parsing button with loading state
|
||||
- [ ] JSON preview pane with syntax highlighting
|
||||
- [ ] Rule validation feedback
|
||||
- [ ] Save/cancel actions
|
||||
- [ ] **Create RuleEditor component** (`/src/components/rules/RuleEditor.jsx`)
|
||||
- [ ] Rich text input with auto-resize
|
||||
- [ ] Character count and validation
|
||||
- [ ] Template suggestions dropdown
|
||||
- [ ] **Create RulePreview component** (`/src/components/rules/RulePreview.jsx`)
|
||||
- [ ] JSON syntax highlighting (use `react-json-view`)
|
||||
- [ ] Editable JSON with validation
|
||||
- [ ] Diff view for rule changes
|
||||
- [ ] **Create RulesList component** (`/src/components/rules/RulesList.jsx`)
|
||||
- [ ] Rule set selection dropdown
|
||||
- [ ] Version history per rule set
|
||||
- [ ] Delete/duplicate rule sets
|
||||
- [ ] **API Integration**
|
||||
- [ ] `POST /api/rules` - Create new rule set
|
||||
- [ ] `PUT /api/rules/{id}` - Update rule set
|
||||
- [ ] `GET /api/rules` - List all rule sets
|
||||
- [ ] `POST /api/rules/{id}/parse` - AI parsing endpoint
|
||||
|
||||
### 2. Plan Generation Workflow
|
||||
- [ ] **Create PlanGeneration page** (`/src/pages/PlanGeneration.jsx`)
|
||||
- [ ] Goal selection interface
|
||||
- [ ] Rule set selection
|
||||
- [ ] Plan parameters (duration, weekly hours)
|
||||
- [ ] Progress tracking for AI generation
|
||||
- [ ] **Create GoalSelector component** (`/src/components/plans/GoalSelector.jsx`)
|
||||
- [ ] Predefined goal templates
|
||||
- [ ] Custom goal input
|
||||
- [ ] Goal validation
|
||||
- [ ] **Create PlanParameters component** (`/src/components/plans/PlanParameters.jsx`)
|
||||
- [ ] Duration slider (4-20 weeks)
|
||||
- [ ] Weekly hours slider (5-15 hours)
|
||||
- [ ] Difficulty level selection
|
||||
- [ ] Available days checkboxes
|
||||
- [ ] **Enhance PlanTimeline component**
|
||||
- [ ] Week-by-week breakdown
|
||||
- [ ] Workout details expandable cards
|
||||
- [ ] Progress tracking indicators
|
||||
- [ ] Edit individual workouts
|
||||
- [ ] **API Integration**
|
||||
- [ ] `POST /api/plans/generate` - Generate new plan
|
||||
- [ ] `GET /api/plans/{id}/preview` - Preview before saving
|
||||
- [ ] Plan generation status polling
|
||||
|
||||
### 3. Route Management & Visualization
|
||||
- [ ] **Enhance RoutesPage** (`/src/pages/RoutesPage.jsx`)
|
||||
- [ ] Route list with metadata
|
||||
- [ ] GPX file upload integration
|
||||
- [ ] Route preview cards
|
||||
- [ ] Search and filter functionality
|
||||
- [ ] **Create RouteVisualization component** (`/src/components/routes/RouteVisualization.jsx`)
|
||||
- [ ] Interactive map (use Leaflet.js)
|
||||
- [ ] GPX track overlay
|
||||
- [ ] Elevation profile chart
|
||||
- [ ] Distance markers
|
||||
- [ ] **Create RouteMetadata component** (`/src/components/routes/RouteMetadata.jsx`)
|
||||
- [ ] Distance, elevation gain, grade analysis
|
||||
- [ ] Estimated time calculations
|
||||
- [ ] Difficulty rating
|
||||
- [ ] Notes/description editing
|
||||
- [ ] **Create SectionManager component** (`/src/components/routes/SectionManager.jsx`)
|
||||
- [ ] Split routes into sections
|
||||
- [ ] Section-specific metadata
|
||||
- [ ] Gear recommendations per section
|
||||
- [ ] **Dependencies to add**
|
||||
- [ ] `npm install leaflet react-leaflet`
|
||||
- [ ] GPX parsing library integration
|
||||
|
||||
### 4. Export/Import System
|
||||
- [ ] **Create ExportImport page** (`/src/pages/ExportImport.jsx`)
|
||||
- [ ] Export options (JSON, ZIP)
|
||||
- [ ] Import validation
|
||||
- [ ] Bulk operations
|
||||
- [ ] **Create DataExporter component** (`/src/components/export/DataExporter.jsx`)
|
||||
- [ ] Selective export (routes, rules, plans)
|
||||
- [ ] Format selection (JSON, GPX, ZIP)
|
||||
- [ ] Export progress tracking
|
||||
- [ ] **Create DataImporter component** (`/src/components/export/DataImporter.jsx`)
|
||||
- [ ] File validation and preview
|
||||
- [ ] Conflict resolution interface
|
||||
- [ ] Import progress tracking
|
||||
- [ ] **API Integration**
|
||||
- [ ] `GET /api/export` - Generate export package
|
||||
- [ ] `POST /api/import` - Import data package
|
||||
- [ ] `POST /api/import/validate` - Validate before import
|
||||
|
||||
## 🔧 Code Quality & Architecture Improvements
|
||||
|
||||
### 5. Enhanced Error Handling
|
||||
- [ ] **Create GlobalErrorHandler** (`/src/components/GlobalErrorHandler.jsx`)
|
||||
- [ ] Centralized error logging
|
||||
- [ ] User-friendly error messages
|
||||
- [ ] Retry mechanisms
|
||||
- [ ] **Improve API error handling**
|
||||
- [ ] Consistent error response format
|
||||
- [ ] Network error recovery
|
||||
- [ ] Timeout handling
|
||||
- [ ] **Add error boundaries**
|
||||
- [ ] Page-level error boundaries
|
||||
- [ ] Component-level error recovery
|
||||
|
||||
### 6. State Management Improvements
|
||||
- [ ] **Enhance AuthContext**
|
||||
- [ ] Add user preferences
|
||||
- [ ] API caching layer
|
||||
- [ ] Offline capability detection
|
||||
- [ ] **Create AppStateContext** (`/src/context/AppStateContext.jsx`)
|
||||
- [ ] Global loading states
|
||||
- [ ] Toast notifications
|
||||
- [ ] Modal management
|
||||
- [ ] **Add React Query** (Optional but recommended)
|
||||
- [ ] `npm install @tanstack/react-query`
|
||||
- [ ] API data caching
|
||||
- [ ] Background refetching
|
||||
- [ ] Optimistic updates
|
||||
|
||||
### 7. UI/UX Enhancements
|
||||
- [ ] **Improve responsive design**
|
||||
- [ ] Better mobile navigation
|
||||
- [ ] Touch-friendly interactions
|
||||
- [ ] Responsive charts and maps
|
||||
- [ ] **Add loading skeletons**
|
||||
- [ ] Replace generic spinners
|
||||
- [ ] Component-specific skeletons
|
||||
- [ ] Progressive loading
|
||||
- [ ] **Create ConfirmDialog component** (`/src/components/ui/ConfirmDialog.jsx`)
|
||||
- [ ] Delete confirmations
|
||||
- [ ] Destructive action warnings
|
||||
- [ ] Custom confirmation messages
|
||||
- [ ] **Add keyboard shortcuts**
|
||||
- [ ] Navigation shortcuts
|
||||
- [ ] Action shortcuts
|
||||
- [ ] Help overlay
|
||||
|
||||
## 🧪 Testing & Quality Assurance
|
||||
|
||||
### 8. Testing Infrastructure
|
||||
- [ ] **Expand component tests**
|
||||
- [ ] Rules management tests
|
||||
- [ ] Plan generation tests
|
||||
- [ ] Route visualization tests
|
||||
- [ ] **Add integration tests**
|
||||
- [ ] API integration tests
|
||||
- [ ] User workflow tests
|
||||
- [ ] Error scenario tests
|
||||
- [ ] **Performance testing**
|
||||
- [ ] Large dataset handling
|
||||
- [ ] Chart rendering performance
|
||||
- [ ] Memory leak detection
|
||||
|
||||
### 9. Development Experience
|
||||
- [ ] **Add Storybook** (Optional)
|
||||
- [ ] Component documentation
|
||||
- [ ] Design system documentation
|
||||
- [ ] Interactive component testing
|
||||
- [ ] **Improve build process**
|
||||
- [ ] Bundle size optimization
|
||||
- [ ] Dead code elimination
|
||||
- [ ] Tree shaking verification
|
||||
- [ ] **Add development tools**
|
||||
- [ ] React DevTools integration
|
||||
- [ ] Performance monitoring
|
||||
- [ ] Bundle analyzer
|
||||
|
||||
## 📚 Documentation & Dependencies
|
||||
|
||||
### 10. Missing Dependencies
|
||||
```json
|
||||
{
|
||||
"leaflet": "^1.9.4",
|
||||
"react-leaflet": "^4.2.1",
|
||||
"react-json-view": "^1.21.3",
|
||||
"@tanstack/react-query": "^4.32.0",
|
||||
"react-hook-form": "^7.45.0",
|
||||
"react-select": "^5.7.4",
|
||||
"file-saver": "^2.0.5"
|
||||
}
|
||||
```
|
||||
|
||||
### 11. Configuration Files
|
||||
- [ ] **Create environment config** (`/src/config/index.js`)
|
||||
- [ ] API endpoints configuration
|
||||
- [ ] Feature flags
|
||||
- [ ] Environment-specific settings
|
||||
- [ ] **Add TypeScript support** (Optional)
|
||||
- [ ] Convert critical components
|
||||
- [ ] Add type definitions
|
||||
- [ ] Improve IDE support
|
||||
|
||||
## 🚀 Deployment & Performance
|
||||
|
||||
### 12. Production Readiness
|
||||
- [ ] **Optimize bundle size**
|
||||
- [ ] Code splitting implementation
|
||||
- [ ] Lazy loading for routes
|
||||
- [ ] Image optimization
|
||||
- [ ] **Add PWA features** (Optional)
|
||||
- [ ] Service worker
|
||||
- [ ] Offline functionality
|
||||
- [ ] App manifest
|
||||
- [ ] **Performance monitoring**
|
||||
- [ ] Core Web Vitals tracking
|
||||
- [ ] Error tracking integration
|
||||
- [ ] User analytics
|
||||
|
||||
## 📅 Implementation Priority
|
||||
|
||||
### Phase 1 (Week 1-2): Core Missing Features
|
||||
1. Rules Management System
|
||||
2. Plan Generation Workflow
|
||||
3. Enhanced Route Management
|
||||
|
||||
### Phase 2 (Week 3): Data Management
|
||||
1. Export/Import System
|
||||
2. Enhanced Error Handling
|
||||
3. State Management Improvements
|
||||
|
||||
### Phase 3 (Week 4): Polish & Quality
|
||||
1. UI/UX Enhancements
|
||||
2. Testing Infrastructure
|
||||
3. Performance Optimization
|
||||
|
||||
### Phase 4 (Ongoing): Maintenance
|
||||
1. Documentation
|
||||
2. Monitoring
|
||||
3. User Feedback Integration
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Success Criteria
|
||||
|
||||
- [ ] All design document workflows implemented
|
||||
- [ ] 90%+ component test coverage
|
||||
- [ ] Mobile-responsive design
|
||||
- [ ] Sub-3s initial page load
|
||||
- [ ] Accessibility compliance (WCAG 2.1 AA)
|
||||
- [ ] Cross-browser compatibility (Chrome, Firefox, Safari, Edge)
|
||||
|
||||
## 📝 Notes
|
||||
|
||||
- **Prioritize user-facing features** over internal architecture improvements
|
||||
- **Test each feature** as you implement it
|
||||
- **Consider Progressive Web App features** for offline functionality
|
||||
- **Plan for internationalization** if expanding globally
|
||||
- **Monitor bundle size** as you add dependencies
|
||||
File diff suppressed because it is too large
Load Diff
97
CL_plan.md
97
CL_plan.md
@@ -1,97 +0,0 @@
|
||||
### Phase 5: Testing and Deployment (Week 12-13)
|
||||
|
||||
#### Week 12: Testing
|
||||
1. **Backend Testing**
|
||||
- Implement comprehensive unit tests for critical services:
|
||||
- Garmin sync service (mock API responses)
|
||||
- AI service (mock OpenRouter API)
|
||||
- Workflow services (plan generation, evolution)
|
||||
- API endpoint testing with realistic payloads
|
||||
- Error handling and edge case testing
|
||||
- Database operation tests (including rollback scenarios)
|
||||
|
||||
Example test for Garmin service:
|
||||
```python
|
||||
# tests/test_garmin_service.py
|
||||
import pytest
|
||||
from unittest.mock import AsyncMock, patch
|
||||
from app.services.garmin import GarminService
|
||||
from app.exceptions import GarminAuthError
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_garmin_auth_failure():
|
||||
with patch('garth.Client', side_effect=Exception("Auth failed")):
|
||||
service = GarminService()
|
||||
with pytest.raises(GarminAuthError):
|
||||
await service.authenticate()
|
||||
```
|
||||
|
||||
2. **Integration Testing**
|
||||
- Test full Garmin sync workflow: authentication → activity fetch → storage
|
||||
- Verify AI analysis pipeline: workout → analysis → plan evolution
|
||||
- Database transaction tests across multiple operations
|
||||
- File system integration tests (GPX upload/download)
|
||||
|
||||
3. **Frontend Testing**
|
||||
- Component tests using React Testing Library
|
||||
- User workflow tests (upload GPX → generate plan → analyze workout)
|
||||
- API response handling and error display tests
|
||||
- Responsive design verification across devices
|
||||
|
||||
Example component test:
|
||||
```javascript
|
||||
// frontend/src/components/__tests__/GarminSync.test.jsx
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import GarminSync from '../GarminSync';
|
||||
|
||||
test('shows sync status after triggering', async () => {
|
||||
render(<GarminSync />);
|
||||
fireEvent.click(screen.getByText('Sync Recent Activities'));
|
||||
expect(await screen.findByText('Syncing...')).toBeInTheDocument();
|
||||
});
|
||||
```
|
||||
|
||||
4. **Continuous Integration Setup**
|
||||
- Configure GitHub Actions pipeline:
|
||||
- Backend test suite (Python)
|
||||
- Frontend test suite (Jest)
|
||||
- Security scanning (dependencies, secrets)
|
||||
- Docker image builds on successful tests
|
||||
- Automated database migration checks
|
||||
- Test coverage reporting
|
||||
|
||||
#### Week 13: Deployment Preparation
|
||||
1. **Environment Configuration**
|
||||
```bash
|
||||
# .env.production
|
||||
GARMIN_USERNAME=your_garmin_email
|
||||
GARMIN_PASSWORD=your_garmin_password
|
||||
OPENROUTER_API_KEY=your_openrouter_key
|
||||
AI_MODEL=anthropic/claude-3-sonnet-20240229
|
||||
API_KEY=your_secure_api_key
|
||||
```
|
||||
|
||||
2. **Production Docker Setup**
|
||||
- Optimize Dockerfiles for production:
|
||||
- Multi-stage builds
|
||||
- Minimized image sizes
|
||||
- Proper user permissions
|
||||
- Health checks for all services
|
||||
- Resource limits in docker-compose.prod.yml
|
||||
|
||||
3. **Backup Strategy**
|
||||
- Implement daily automated backups:
|
||||
- Database (pg_dump)
|
||||
- GPX files
|
||||
- Garmin sessions
|
||||
- Backup rotation (keep last 30 days)
|
||||
- Verify restore procedure
|
||||
|
||||
4. **Monitoring and Logging**
|
||||
- Structured logging with log rotation
|
||||
- System health dashboard
|
||||
- Error tracking and alerting
|
||||
- Performance monitoring
|
||||
|
||||
## Key Technical Decisions
|
||||
...
|
||||
Binary file not shown.
@@ -20,6 +20,7 @@ AsyncSessionLocal = sessionmaker(
|
||||
expire_on_commit=False
|
||||
)
|
||||
|
||||
# Base is now defined here and imported by models
|
||||
Base = declarative_base()
|
||||
|
||||
async def get_db() -> AsyncSession:
|
||||
|
||||
Binary file not shown.
@@ -1,8 +1,8 @@
|
||||
from datetime import datetime
|
||||
from sqlalchemy import Column, Integer, DateTime
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
|
||||
Base = declarative_base()
|
||||
# Import Base from database.py to ensure models use the same Base instance
|
||||
from ..database import Base
|
||||
|
||||
class BaseModel(Base):
|
||||
__abstract__ = True
|
||||
|
||||
248
designdoc.md
248
designdoc.md
@@ -1,248 +0,0 @@
|
||||
---
|
||||
|
||||
# **AI-Assisted Cycling Coach — Design Document**
|
||||
|
||||
## **1. Architecture Overview**
|
||||
|
||||
**Goal:** Web-based cycling coach that plans workouts, analyzes Garmin rides, and integrates AI while enforcing strict user-defined rules.
|
||||
|
||||
### **Components**
|
||||
|
||||
| Component | Tech | Purpose |
|
||||
| ---------------- | -------------------------- | ------------------------------------------------------------------ |
|
||||
| Frontend | React/Next.js | UI for routes, plans, analysis, file uploads |
|
||||
| Backend | Python (FastAPI, async) | API layer, AI integration, Garmin sync, DB access |
|
||||
| Database | PostgreSQL | Stores routes, sections, plans, rules, workouts, prompts, analyses |
|
||||
| File Storage | Mounted folder `/data/gpx` | Store GPX files for sections/routes |
|
||||
| AI Integration | OpenRouter via backend | Plan generation, workout analysis, suggestions |
|
||||
| Containerization | Docker + docker-compose | Encapsulate frontend, backend, database with persistent storage |
|
||||
|
||||
**Workflow Overview**
|
||||
|
||||
1. Upload/import GPX → backend saves to mounted folder + metadata in DB
|
||||
2. Define plaintext rules → Store directly in DB
|
||||
3. Generate plan → AI creates JSON plan → DB versioned
|
||||
4. Ride recorded on Garmin → backend syncs activity metrics → stores in DB
|
||||
5. AI analyzes workout → feedback & suggestions stored → user approves → new plan version created
|
||||
|
||||
---
|
||||
|
||||
## **2. Backend Design (Python, Async)**
|
||||
|
||||
**Framework:** FastAPI (async-first, non-blocking I/O)
|
||||
**Tasks:**
|
||||
|
||||
* **Route/Section Management:** Upload GPX, store metadata, read GPX files for visualization
|
||||
* **Rule Management:** CRUD rules with plaintext storage
|
||||
* **Plan Management:** Generate plans (AI), store versions
|
||||
* **Workout Analysis:** Fetch Garmin activity, run AI analysis, store reports
|
||||
* **AI Integration:** Async calls to OpenRouter
|
||||
* **Database Interaction:** Async Postgres client (e.g., `asyncpg` or `SQLAlchemy Async`)
|
||||
|
||||
**Endpoints (examples)**
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
| ------ | ------------------- | ------------------------------------------------ |
|
||||
| POST | `/routes/upload` | Upload GPX file for route/section |
|
||||
| GET | `/routes` | List routes and sections |
|
||||
| POST | `/rules` | Create new rule set (plaintext) |
|
||||
| POST | `/plans/generate` | Generate new plan using rules & goals |
|
||||
| GET | `/plans/{plan_id}` | Fetch plan JSON & version info |
|
||||
| POST | `/workouts/analyze` | Trigger AI analysis for a synced Garmin activity |
|
||||
| POST | `/workouts/approve` | Approve AI suggestions → create new plan version |
|
||||
|
||||
**Async Patterns:**
|
||||
|
||||
* File I/O → async reading/writing GPX
|
||||
* AI API calls → async HTTP requests
|
||||
* Garmin sync → async polling/scheduled jobs
|
||||
|
||||
---
|
||||
|
||||
## **3. Database Design (Postgres)**
|
||||
|
||||
**Tables:**
|
||||
|
||||
```sql
|
||||
-- Routes & Sections
|
||||
CREATE TABLE routes (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE sections (
|
||||
id SERIAL PRIMARY KEY,
|
||||
route_id INT REFERENCES routes(id),
|
||||
gpx_file_path TEXT NOT NULL,
|
||||
distance_m NUMERIC,
|
||||
grade_avg NUMERIC,
|
||||
min_gear TEXT,
|
||||
est_time_minutes NUMERIC,
|
||||
created_at TIMESTAMP DEFAULT now()
|
||||
);
|
||||
|
||||
-- Rules (plaintext storage)
|
||||
CREATE TABLE rules (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
user_defined BOOLEAN DEFAULT true,
|
||||
rule_text TEXT NOT NULL, -- Plaintext rules
|
||||
version INT DEFAULT 1,
|
||||
parent_rule_id INT REFERENCES rules(id),
|
||||
created_at TIMESTAMP DEFAULT now(),
|
||||
updated_at TIMESTAMP DEFAULT now()
|
||||
);
|
||||
|
||||
-- Plans (versioned)
|
||||
CREATE TABLE plans (
|
||||
id SERIAL PRIMARY KEY,
|
||||
jsonb_plan JSONB NOT NULL,
|
||||
version INT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT now()
|
||||
);
|
||||
|
||||
-- Workouts
|
||||
CREATE TABLE workouts (
|
||||
id SERIAL PRIMARY KEY,
|
||||
plan_id INT REFERENCES plans(id),
|
||||
garmin_activity_id TEXT NOT NULL,
|
||||
metrics JSONB,
|
||||
created_at TIMESTAMP DEFAULT now()
|
||||
);
|
||||
|
||||
-- Analyses
|
||||
CREATE TABLE analyses (
|
||||
id SERIAL PRIMARY KEY,
|
||||
workout_id INT REFERENCES workouts(id),
|
||||
jsonb_feedback JSONB,
|
||||
created_at TIMESTAMP DEFAULT now()
|
||||
);
|
||||
|
||||
-- AI Prompts
|
||||
CREATE TABLE prompts (
|
||||
id SERIAL PRIMARY KEY,
|
||||
action_type TEXT, -- plan, analysis, suggestion
|
||||
model TEXT,
|
||||
prompt_text TEXT,
|
||||
version INT DEFAULT 1,
|
||||
created_at TIMESTAMP DEFAULT now()
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## **4. Containerization (Docker Compose)**
|
||||
|
||||
```yaml
|
||||
version: '3.9'
|
||||
services:
|
||||
backend:
|
||||
build: ./backend
|
||||
ports:
|
||||
- "8000:8000"
|
||||
volumes:
|
||||
- gpx-data:/app/data/gpx
|
||||
environment:
|
||||
- DATABASE_URL=postgresql://postgres:password@db:5432/cycling
|
||||
depends_on:
|
||||
- db
|
||||
|
||||
frontend:
|
||||
build: ./frontend
|
||||
ports:
|
||||
- "3000:3000"
|
||||
|
||||
db:
|
||||
image: postgres:15
|
||||
restart: always
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: password
|
||||
POSTGRES_DB: cycling
|
||||
volumes:
|
||||
- postgres-data:/var/lib/postgresql/data
|
||||
|
||||
volumes:
|
||||
gpx-data:
|
||||
driver: local
|
||||
postgres-data:
|
||||
driver: local
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
|
||||
* `/app/data/gpx` inside backend container is persisted on host via `gpx-data` volume.
|
||||
* Postgres data persisted via `postgres-data`.
|
||||
* Backend talks to DB via async client.
|
||||
|
||||
---
|
||||
|
||||
## **5. Frontend UI Layouts & Flows**
|
||||
|
||||
### **5.1 Layout**
|
||||
|
||||
* **Navbar:** Routes | Rules | Plans | Workouts | Analysis | Export/Import
|
||||
* **Sidebar:** Filters (date, type, difficulty)
|
||||
* **Main Area:** Dynamic content depending on selection
|
||||
|
||||
### **5.2 Key Screens**
|
||||
|
||||
1. **Routes**
|
||||
|
||||
* Upload/import GPX
|
||||
* View route map + section metadata
|
||||
2. **Rules**
|
||||
|
||||
* Plaintext rule editor
|
||||
* Simple create/edit form
|
||||
* Rule version history
|
||||
3. **Plan**
|
||||
|
||||
* Select goal + rule set → generate plan
|
||||
* View plan timeline & weekly workouts
|
||||
4. **Workout Analysis**
|
||||
|
||||
* List synced Garmin activities
|
||||
* Select activity → AI generates report
|
||||
* Visualizations: HR, cadence, power vs planned
|
||||
* Approve suggestions → new plan version
|
||||
5. **Export/Import**
|
||||
|
||||
* Export JSON/ZIP of routes, rules, plans
|
||||
* Import JSON/GPX
|
||||
|
||||
### **5.3 User Flow Example**
|
||||
|
||||
1. Upload GPX → backend saves file + DB metadata
|
||||
2. Define rule set → Store plaintext → DB versioned
|
||||
3. Generate plan → AI → store plan version in DB
|
||||
4. Sync Garmin activity → backend fetches metrics → store workout
|
||||
5. AI analyzes → report displayed → user approves → new plan version
|
||||
6. Export plan or route as needed
|
||||
|
||||
---
|
||||
|
||||
## **6. AI Integration**
|
||||
|
||||
* Each **action type** (plan generation, analysis, suggestion) has:
|
||||
|
||||
* Stored prompt template in DB
|
||||
* Configurable model per action
|
||||
* Async calls to OpenRouter
|
||||
* Store raw AI output + processed structured result in DB
|
||||
* Use plaintext rules directly in prompts without parsing
|
||||
|
||||
---
|
||||
|
||||
## ✅ **Next Steps**
|
||||
|
||||
1. Implement **Python FastAPI backend** with async patterns.
|
||||
2. Build **Postgres DB schema** and migration scripts.
|
||||
3. Setup **Docker Compose** with mounted GPX folder.
|
||||
4. Design frontend UI based on the flows above.
|
||||
5. Integrate AI endpoints and Garmin sync.
|
||||
|
||||
---
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
# Export/Import API Specification
|
||||
|
||||
## Export Endpoint
|
||||
`GET /api/export`
|
||||
|
||||
### Parameters (query string)
|
||||
- `types` (required): Comma-separated list of data types to export
|
||||
- Valid values: `routes`, `rules`, `plans`, `all`
|
||||
- `format` (required): Export format
|
||||
- `json`: Single JSON file
|
||||
- `zip`: ZIP archive with separate files
|
||||
- `gpx`: Only GPX files (routes only)
|
||||
|
||||
### Response
|
||||
- `200 OK` with file download
|
||||
- `400 Bad Request` for invalid parameters
|
||||
- `500 Internal Server Error` for export failures
|
||||
|
||||
### Example
|
||||
```http
|
||||
GET /api/export?types=routes,plans&format=zip
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Import Validation
|
||||
`POST /api/import/validate`
|
||||
|
||||
### Request
|
||||
- Multipart form with `file` field containing import data
|
||||
|
||||
### Response
|
||||
```json
|
||||
{
|
||||
"valid": true,
|
||||
"conflicts": [
|
||||
{
|
||||
"type": "route",
|
||||
"id": 123,
|
||||
"name": "Mountain Loop",
|
||||
"existing_version": 2,
|
||||
"import_version": 3,
|
||||
"resolution_options": ["overwrite", "rename", "skip"]
|
||||
}
|
||||
],
|
||||
"summary": {
|
||||
"routes": 15,
|
||||
"rules": 3,
|
||||
"plans": 2
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Import Execution
|
||||
`POST /api/import`
|
||||
|
||||
### Request
|
||||
- Multipart form with:
|
||||
- `file`: Import data file
|
||||
- `conflict_resolution`: Global strategy (overwrite, skip, rename)
|
||||
- `resolutions`: JSON array of specific resolutions (optional)
|
||||
```json
|
||||
[{"type": "route", "id": 123, "action": "overwrite"}]
|
||||
```
|
||||
|
||||
### Response
|
||||
```json
|
||||
{
|
||||
"imported": {
|
||||
"routes": 12,
|
||||
"rules": 3,
|
||||
"plans": 2
|
||||
},
|
||||
"skipped": {
|
||||
"routes": 3,
|
||||
"rules": 0,
|
||||
"plans": 0
|
||||
},
|
||||
"errors": []
|
||||
}
|
||||
```
|
||||
|
||||
### Status Codes
|
||||
- `200 OK`: Import completed
|
||||
- `202 Accepted`: Import in progress (async)
|
||||
- `400 Bad Request`: Invalid input
|
||||
- `409 Conflict`: Unresolved conflicts
|
||||
|
||||
---
|
||||
@@ -1,221 +0,0 @@
|
||||
# Export/Import Frontend Implementation
|
||||
|
||||
## File Structure
|
||||
```
|
||||
src/pages/
|
||||
ExportImport.jsx # Main page
|
||||
|
||||
src/components/export/
|
||||
DataExporter.jsx # Export functionality
|
||||
DataImporter.jsx # Import functionality
|
||||
ConflictDialog.jsx # Conflict resolution UI
|
||||
ImportSummary.jsx # Post-import report
|
||||
```
|
||||
|
||||
## Component Specifications
|
||||
|
||||
### ExportImport.jsx
|
||||
```jsx
|
||||
import { useState } from 'react';
|
||||
import DataExporter from '../components/export/DataExporter';
|
||||
import DataImporter from '../components/export/DataImporter';
|
||||
|
||||
export default function ExportImportPage() {
|
||||
const [activeTab, setActiveTab] = useState('export');
|
||||
|
||||
return (
|
||||
<div className="export-import-page">
|
||||
<div className="tabs">
|
||||
<button onClick={() => setActiveTab('export')}>Export</button>
|
||||
<button onClick={() => setActiveTab('import')}>Import</button>
|
||||
</div>
|
||||
|
||||
{activeTab === 'export' ? <DataExporter /> : <DataImporter />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### DataExporter.jsx
|
||||
```jsx
|
||||
import { useState } from 'react';
|
||||
|
||||
const EXPORT_TYPES = [
|
||||
{ id: 'routes', label: 'Routes' },
|
||||
{ id: 'rules', label: 'Training Rules' },
|
||||
{ id: 'plans', label: 'Training Plans' }
|
||||
];
|
||||
|
||||
const EXPORT_FORMATS = [
|
||||
{ id: 'json', label: 'JSON' },
|
||||
{ id: 'zip', label: 'ZIP Archive' },
|
||||
{ id: 'gpx', label: 'GPX Files' }
|
||||
];
|
||||
|
||||
export default function DataExporter() {
|
||||
const [selectedTypes, setSelectedTypes] = useState([]);
|
||||
const [selectedFormat, setSelectedFormat] = useState('json');
|
||||
const [isExporting, setIsExporting] = useState(false);
|
||||
const [progress, setProgress] = useState(0);
|
||||
|
||||
const handleExport = async () => {
|
||||
setIsExporting(true);
|
||||
// API call to /api/export?types=...&format=...
|
||||
// Track progress and trigger download
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="exporter">
|
||||
<h2>Export Data</h2>
|
||||
|
||||
<div className="type-selection">
|
||||
<h3>Select Data to Export</h3>
|
||||
{EXPORT_TYPES.map(type => (
|
||||
<label key={type.id}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedTypes.includes(type.id)}
|
||||
onChange={() => toggleType(type.id)}
|
||||
/>
|
||||
{type.label}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="format-selection">
|
||||
<h3>Export Format</h3>
|
||||
<select value={selectedFormat} onChange={e => setSelectedFormat(e.target.value)}>
|
||||
{EXPORT_FORMATS.map(format => (
|
||||
<option key={format.id} value={format.id}>{format.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleExport}
|
||||
disabled={selectedTypes.length === 0 || isExporting}
|
||||
>
|
||||
{isExporting ? `Exporting... ${progress}%` : 'Export Data'}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### DataImporter.jsx
|
||||
```jsx
|
||||
import { useState } from 'react';
|
||||
import ConflictDialog from './ConflictDialog';
|
||||
|
||||
export default function DataImporter() {
|
||||
const [file, setFile] = useState(null);
|
||||
const [validation, setValidation] = useState(null);
|
||||
const [isImporting, setIsImporting] = useState(false);
|
||||
const [showConflictDialog, setShowConflictDialog] = useState(false);
|
||||
|
||||
const handleFileUpload = (e) => {
|
||||
const file = e.target.files[0];
|
||||
setFile(file);
|
||||
// Call /api/import/validate
|
||||
// Set validation results
|
||||
};
|
||||
|
||||
const handleImport = () => {
|
||||
if (validation?.conflicts?.length > 0) {
|
||||
setShowConflictDialog(true);
|
||||
} else {
|
||||
startImport();
|
||||
}
|
||||
};
|
||||
|
||||
const startImport = (resolutions = []) => {
|
||||
setIsImporting(true);
|
||||
// Call /api/import with conflict resolutions
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="importer">
|
||||
<h2>Import Data</h2>
|
||||
|
||||
<input type="file" onChange={handleFileUpload} />
|
||||
|
||||
{validation && (
|
||||
<div className="validation-results">
|
||||
<h3>Validation Results</h3>
|
||||
<p>Found: {validation.summary.routes} routes,
|
||||
{validation.summary.rules} rules,
|
||||
{validation.summary.plans} plans</p>
|
||||
{validation.conflicts.length > 0 && (
|
||||
<p>⚠️ {validation.conflicts.length} conflicts detected</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={handleImport}
|
||||
disabled={!file || isImporting}
|
||||
>
|
||||
{isImporting ? 'Importing...' : 'Import Data'}
|
||||
</button>
|
||||
|
||||
{showConflictDialog && (
|
||||
<ConflictDialog
|
||||
conflicts={validation.conflicts}
|
||||
onResolve={startImport}
|
||||
onCancel={() => setShowConflictDialog(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### ConflictDialog.jsx
|
||||
```jsx
|
||||
export default function ConflictDialog({ conflicts, onResolve, onCancel }) {
|
||||
const [resolutions, setResolutions] = useState({});
|
||||
|
||||
const handleResolution = (id, action) => {
|
||||
setResolutions(prev => ({ ...prev, [id]: action }));
|
||||
};
|
||||
|
||||
const applyResolutions = () => {
|
||||
const resolutionList = Object.entries(resolutions).map(([id, action]) => ({
|
||||
id,
|
||||
action
|
||||
}));
|
||||
onResolve(resolutionList);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="conflict-dialog">
|
||||
<h3>Resolve Conflicts</h3>
|
||||
<div className="conflicts-list">
|
||||
{conflicts.map(conflict => (
|
||||
<div key={conflict.id} className="conflict-item">
|
||||
<h4>{conflict.name} ({conflict.type})</h4>
|
||||
<p>Existing version: {conflict.existing_version}</p>
|
||||
<p>Import version: {conflict.import_version}</p>
|
||||
<select
|
||||
value={resolutions[conflict.id] || 'skip'}
|
||||
onChange={e => handleResolution(conflict.id, e.target.value)}
|
||||
>
|
||||
<option value="overwrite">Overwrite</option>
|
||||
<option value="rename">Rename</option>
|
||||
<option value="skip">Skip</option>
|
||||
</select>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="actions">
|
||||
<button onClick={onCancel}>Cancel</button>
|
||||
<button onClick={applyResolutions}>Apply Resolutions</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Dependencies to Install
|
||||
```bash
|
||||
npm install react-dropzone react-json-view file-saver
|
||||
23
main.py
23
main.py
@@ -18,7 +18,8 @@ from textual.logging import TextualHandler
|
||||
|
||||
from backend.app.config import settings
|
||||
from backend.app.database import init_db
|
||||
from tui.views.dashboard import DashboardView
|
||||
# Use working dashboard with static content
|
||||
from tui.views.dashboard_working import WorkingDashboardView as DashboardView
|
||||
from tui.views.workouts import WorkoutView
|
||||
from tui.views.plans import PlanView
|
||||
from tui.views.rules import RuleView
|
||||
@@ -125,14 +126,6 @@ class CyclingCoachApp(App):
|
||||
|
||||
async def on_mount(self) -> None:
|
||||
"""Initialize the application when mounted."""
|
||||
# Initialize database
|
||||
try:
|
||||
await init_db()
|
||||
self.log("Database initialized successfully")
|
||||
except Exception as e:
|
||||
self.log(f"Database initialization failed: {e}", severity="error")
|
||||
self.exit(1)
|
||||
|
||||
# Set initial active navigation
|
||||
self.query_one("#nav-dashboard").add_class("-active")
|
||||
|
||||
@@ -175,7 +168,17 @@ async def main():
|
||||
data_dir.mkdir(exist_ok=True)
|
||||
(data_dir / "gpx").mkdir(exist_ok=True)
|
||||
(data_dir / "sessions").mkdir(exist_ok=True)
|
||||
|
||||
|
||||
# Initialize database BEFORE starting the app
|
||||
try:
|
||||
await init_db()
|
||||
print("Database initialized successfully") # Use print as app logging isn't available yet
|
||||
except Exception as e:
|
||||
print(f"Database initialization failed: {e}")
|
||||
# Exit if database initialization fails
|
||||
import sys
|
||||
sys.exit(1)
|
||||
|
||||
# Run the TUI application
|
||||
app = CyclingCoachApp()
|
||||
await app.run_async()
|
||||
|
||||
@@ -5,7 +5,7 @@ alembic>=1.13.1
|
||||
pydantic-settings==2.2.1
|
||||
|
||||
# TUI framework
|
||||
textual==0.82.0
|
||||
textual
|
||||
|
||||
# Data processing
|
||||
gpxpy # GPX parsing library
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
45
tui/views/base_view.py
Normal file
45
tui/views/base_view.py
Normal file
@@ -0,0 +1,45 @@
|
||||
"""
|
||||
Base view class for TUI application with common async utilities.
|
||||
"""
|
||||
from textual.app import ComposeResult
|
||||
from textual.widget import Widget
|
||||
from textual.worker import Worker, get_current_worker
|
||||
from textual import work, on
|
||||
from typing import Callable, Any, Coroutine, Optional
|
||||
from tui.widgets.error_modal import ErrorModal
|
||||
|
||||
class BaseView(Widget):
|
||||
"""Base view class with async utilities that all views should inherit from."""
|
||||
|
||||
def run_async(self, coro: Coroutine, callback: Callable[[Any], None] = None) -> Worker:
|
||||
"""Run an async task in the background with proper error handling."""
|
||||
worker = self.run_worker(
|
||||
self._async_wrapper(coro, callback),
|
||||
exclusive=True,
|
||||
group="db_operations"
|
||||
)
|
||||
return worker
|
||||
|
||||
@work(thread=True)
|
||||
async def _async_wrapper(self, coro: Coroutine, callback: Callable[[Any], None] = None) -> None:
|
||||
"""Wrapper for async operations with cancellation support."""
|
||||
try:
|
||||
result = await coro
|
||||
if callback:
|
||||
self.call_after_refresh(callback, result)
|
||||
except Exception as e:
|
||||
self.log(f"Async operation failed: {str(e)}", severity="error")
|
||||
self.app.bell()
|
||||
self.call_after_refresh(
|
||||
self.show_error,
|
||||
str(e),
|
||||
lambda: self.run_async(coro, callback)
|
||||
)
|
||||
finally:
|
||||
worker = get_current_worker()
|
||||
if worker and worker.is_cancelled:
|
||||
self.log("Async operation cancelled")
|
||||
|
||||
def show_error(self, message: str, retry_action: Optional[Callable] = None) -> None:
|
||||
"""Display error modal with retry option."""
|
||||
self.app.push_screen(ErrorModal(message, retry_action))
|
||||
@@ -98,6 +98,10 @@ class DashboardView(Widget):
|
||||
"""Create dashboard layout."""
|
||||
self.log(f"[DashboardView] compose called | debug_id={self.debug_id} | loading={self.loading} | error={self.error_message}")
|
||||
yield Static("AI Cycling Coach Dashboard", classes="view-title")
|
||||
|
||||
# DEBUG: Always show some content to verify rendering
|
||||
yield Static(f"DEBUG: View Status - Loading: {self.loading}, Error: {bool(self.error_message)}, Data: {bool(self.dashboard_data)}")
|
||||
|
||||
# Always show the structure - use conditional content
|
||||
if self.error_message:
|
||||
with Container(classes="error-container"):
|
||||
@@ -115,6 +119,7 @@ class DashboardView(Widget):
|
||||
yield Static("Click Refresh to try again", classes="error-action")
|
||||
elif self.loading and not self.dashboard_data:
|
||||
# Initial load - full screen loader
|
||||
yield Static("Loading dashboard data...")
|
||||
yield LoadingIndicator(id="dashboard-loader")
|
||||
else:
|
||||
# Show content with optional refresh indicator
|
||||
|
||||
90
tui/views/dashboard_working.py
Normal file
90
tui/views/dashboard_working.py
Normal file
@@ -0,0 +1,90 @@
|
||||
"""
|
||||
Working Dashboard view for AI Cycling Coach TUI.
|
||||
Simple version that displays content without complex async loading.
|
||||
"""
|
||||
from textual.app import ComposeResult
|
||||
from textual.containers import Container, Horizontal, Vertical, ScrollableContainer
|
||||
from textual.widgets import Static, DataTable
|
||||
from textual.widget import Widget
|
||||
|
||||
|
||||
class WorkingDashboardView(Widget):
|
||||
"""Simple working dashboard view."""
|
||||
|
||||
DEFAULT_CSS = """
|
||||
.view-title {
|
||||
text-align: center;
|
||||
color: $accent;
|
||||
text-style: bold;
|
||||
margin-bottom: 1;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
text-style: bold;
|
||||
color: $primary;
|
||||
margin: 1 0;
|
||||
}
|
||||
|
||||
.dashboard-column {
|
||||
width: 1fr;
|
||||
margin: 0 1;
|
||||
}
|
||||
|
||||
.stats-container {
|
||||
border: solid $primary;
|
||||
padding: 1;
|
||||
margin: 1 0;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
margin: 0 1;
|
||||
}
|
||||
"""
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
"""Create dashboard layout with static content."""
|
||||
yield Static("AI Cycling Coach Dashboard", classes="view-title")
|
||||
|
||||
with ScrollableContainer():
|
||||
with Horizontal():
|
||||
# Left column - Recent workouts
|
||||
with Vertical(classes="dashboard-column"):
|
||||
yield Static("Recent Workouts", classes="section-title")
|
||||
workout_table = DataTable(id="recent-workouts")
|
||||
workout_table.add_columns("Date", "Type", "Duration", "Distance", "Avg HR")
|
||||
|
||||
# Add sample data
|
||||
workout_table.add_row("12/08 14:30", "Cycling", "75min", "32.5km", "145bpm")
|
||||
workout_table.add_row("12/06 09:15", "Cycling", "90min", "45.2km", "138bpm")
|
||||
workout_table.add_row("12/04 16:45", "Cycling", "60min", "25.8km", "152bpm")
|
||||
workout_table.add_row("12/02 10:00", "Cycling", "120min", "68.1km", "141bpm")
|
||||
|
||||
yield workout_table
|
||||
|
||||
# Right column - Quick stats and current plan
|
||||
with Vertical(classes="dashboard-column"):
|
||||
# Weekly stats
|
||||
with Container(classes="stats-container"):
|
||||
yield Static("This Week", classes="section-title")
|
||||
yield Static("Workouts: 4", classes="stat-item")
|
||||
yield Static("Distance: 171.6 km", classes="stat-item")
|
||||
yield Static("Time: 5h 45m", classes="stat-item")
|
||||
|
||||
# Active plan
|
||||
with Container(classes="stats-container"):
|
||||
yield Static("Current Plan", classes="section-title")
|
||||
yield Static("Base Building v1 (Created: 12/01)", classes="stat-item")
|
||||
yield Static("Week 2 of 4 - On Track", classes="stat-item")
|
||||
|
||||
# Sync status
|
||||
with Container(classes="stats-container"):
|
||||
yield Static("Garmin Sync", classes="section-title")
|
||||
yield Static("Status: Connected ✅", classes="stat-item")
|
||||
yield Static("Last: 12/08 15:30 (4 activities)", classes="stat-item")
|
||||
|
||||
# Database status
|
||||
with Container(classes="stats-container"):
|
||||
yield Static("System Status", classes="section-title")
|
||||
yield Static("Database: ✅ Connected", classes="stat-item")
|
||||
yield Static("Tables: ✅ All created", classes="stat-item")
|
||||
yield Static("Views: ✅ Working correctly!", classes="stat-item")
|
||||
@@ -2,6 +2,7 @@
|
||||
Plan view for AI Cycling Coach TUI.
|
||||
Displays training plans, plan generation, and plan management.
|
||||
"""
|
||||
import asyncio
|
||||
from textual.app import ComposeResult
|
||||
from textual.containers import Container, Horizontal, Vertical, ScrollableContainer
|
||||
from textual.widgets import (
|
||||
@@ -16,6 +17,7 @@ from typing import List, Dict, Optional
|
||||
from backend.app.database import AsyncSessionLocal
|
||||
from tui.services.plan_service import PlanService
|
||||
from tui.services.rule_service import RuleService
|
||||
from .base_view import BaseView
|
||||
|
||||
|
||||
class PlanGenerationForm(Widget):
|
||||
@@ -71,7 +73,7 @@ class PlanDetailsModal(Widget):
|
||||
yield Button("Edit Plan", id="edit-plan-btn", variant="primary")
|
||||
|
||||
|
||||
class PlanView(Widget):
|
||||
class PlanView(BaseView):
|
||||
"""Training plan management view."""
|
||||
|
||||
# Reactive attributes
|
||||
@@ -154,33 +156,43 @@ class PlanView(Widget):
|
||||
with Container():
|
||||
yield PlanGenerationForm()
|
||||
|
||||
async def on_mount(self) -> None:
|
||||
def on_mount(self) -> None:
|
||||
"""Load plan data when mounted."""
|
||||
self.loading = True
|
||||
asyncio.create_task(self._load_plans_and_handle_result())
|
||||
|
||||
async def _load_plans_and_handle_result(self) -> None:
|
||||
"""Load plans data and handle the result."""
|
||||
try:
|
||||
await self.load_plans_data()
|
||||
plans, rules = await self._load_plans_data()
|
||||
self.plans = plans
|
||||
self.rules = rules
|
||||
self.loading = False
|
||||
self.refresh(layout=True)
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"Plans loading error: {e}", severity="error")
|
||||
self.log(f"Error loading plans data: {e}", severity="error")
|
||||
self.loading = False
|
||||
self.refresh()
|
||||
|
||||
async def load_plans_data(self) -> None:
|
||||
"""Load plans and rules data."""
|
||||
async def _load_plans_data(self) -> tuple[list, list]:
|
||||
"""Load plans and rules data (async worker)."""
|
||||
async with AsyncSessionLocal() as db:
|
||||
plan_service = PlanService(db)
|
||||
rule_service = RuleService(db)
|
||||
return (
|
||||
await plan_service.get_plans(),
|
||||
await rule_service.get_rules()
|
||||
)
|
||||
|
||||
def on_plans_loaded(self, result: tuple[list, list]) -> None:
|
||||
"""Handle loaded plans data."""
|
||||
try:
|
||||
async with AsyncSessionLocal() as db:
|
||||
plan_service = PlanService(db)
|
||||
rule_service = RuleService(db)
|
||||
|
||||
# Load plans and rules
|
||||
self.plans = await plan_service.get_plans()
|
||||
self.rules = await rule_service.get_rules()
|
||||
|
||||
# Update loading state
|
||||
self.loading = False
|
||||
self.refresh()
|
||||
|
||||
# Populate UI elements
|
||||
await self.populate_plans_table()
|
||||
await self.populate_rules_select()
|
||||
plans, rules = result
|
||||
self.plans = plans
|
||||
self.rules = rules
|
||||
self.loading = False
|
||||
self.refresh(layout=True)
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"Error loading plans data: {e}", severity="error")
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
Workout view for AI Cycling Coach TUI.
|
||||
Displays workout list, analysis, and import functionality.
|
||||
"""
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
from textual.app import ComposeResult
|
||||
from textual.containers import Container, Horizontal, Vertical, ScrollableContainer
|
||||
@@ -16,6 +17,8 @@ from typing import List, Dict, Optional
|
||||
|
||||
from backend.app.database import AsyncSessionLocal
|
||||
from tui.services.workout_service import WorkoutService
|
||||
from tui.widgets.loading import LoadingSpinner
|
||||
from tui.views.base_view import BaseView
|
||||
|
||||
|
||||
class WorkoutMetricsChart(Widget):
|
||||
@@ -143,7 +146,7 @@ class WorkoutAnalysisPanel(Widget):
|
||||
return "\n".join(formatted)
|
||||
|
||||
|
||||
class WorkoutView(Widget):
|
||||
class WorkoutView(BaseView):
|
||||
"""Workout management view."""
|
||||
|
||||
# Reactive attributes
|
||||
@@ -206,7 +209,7 @@ class WorkoutView(Widget):
|
||||
yield Static("Workout Management", classes="view-title")
|
||||
|
||||
if self.loading:
|
||||
yield LoadingIndicator(id="workouts-loader")
|
||||
yield LoadingSpinner("Loading workouts...")
|
||||
else:
|
||||
with TabbedContent():
|
||||
with TabPane("Workout List", id="workout-list-tab"):
|
||||
@@ -298,33 +301,46 @@ Elevation Gain: {workout.get('elevation_gain_m', 'N/A')} m
|
||||
|
||||
return Static(summary_text)
|
||||
|
||||
async def on_mount(self) -> None:
|
||||
def on_mount(self) -> None:
|
||||
"""Load workout data when mounted."""
|
||||
try:
|
||||
await self.load_workouts_data()
|
||||
except Exception as e:
|
||||
self.log(f"Workouts loading error: {e}", severity="error")
|
||||
self.loading = False
|
||||
self.refresh()
|
||||
|
||||
async def load_workouts_data(self) -> None:
|
||||
"""Load workouts and sync status."""
|
||||
self.loading = True
|
||||
# self.run_worker(self._load_workouts_and_handle_result_sync, thread=True)
|
||||
|
||||
# def _load_workouts_and_handle_result_sync(self) -> None:
|
||||
# """Synchronous wrapper to load workouts data and handle the result."""
|
||||
# try:
|
||||
# # Run the async part using asyncio.run
|
||||
# workouts, sync_status = asyncio.run(self._load_workouts_data())
|
||||
# self.workouts = workouts
|
||||
# self.sync_status = sync_status
|
||||
# self.loading = False
|
||||
# self.call_after_refresh(lambda: self.refresh(layout=True))
|
||||
# except Exception as e:
|
||||
# self.log(f"Error loading workouts data: {e}", severity="error")
|
||||
# self.loading = False
|
||||
# self.call_after_refresh(lambda: self.refresh())
|
||||
|
||||
async def _load_workouts_data(self) -> tuple[list, dict]:
|
||||
"""Load workouts and sync status (async worker)."""
|
||||
try:
|
||||
async with AsyncSessionLocal() as db:
|
||||
workout_service = WorkoutService(db)
|
||||
|
||||
# Load workouts and sync status
|
||||
self.workouts = await workout_service.get_workouts(limit=50)
|
||||
self.sync_status = await workout_service.get_sync_status()
|
||||
|
||||
# Update loading state
|
||||
self.loading = False
|
||||
self.refresh()
|
||||
|
||||
# Populate UI elements
|
||||
await self.populate_workouts_table()
|
||||
await self.update_sync_status()
|
||||
|
||||
return (
|
||||
await workout_service.get_workouts(limit=50),
|
||||
await workout_service.get_sync_status()
|
||||
)
|
||||
except Exception as e:
|
||||
self.log(f"Error loading workouts: {str(e)}", severity="error")
|
||||
raise
|
||||
|
||||
def on_workouts_loaded(self, result: tuple[list, dict]) -> None:
|
||||
"""Handle loaded workout data."""
|
||||
try:
|
||||
workouts, sync_status = result
|
||||
self.workouts = workouts
|
||||
self.sync_status = sync_status
|
||||
self.loading = False
|
||||
self.refresh(layout=True)
|
||||
except Exception as e:
|
||||
self.log(f"Error loading workouts data: {e}", severity="error")
|
||||
self.loading = False
|
||||
|
||||
34
tui/widgets/error_modal.py
Normal file
34
tui/widgets/error_modal.py
Normal file
@@ -0,0 +1,34 @@
|
||||
"""
|
||||
Error modal component for TUI.
|
||||
"""
|
||||
from textual.app import ComposeResult
|
||||
from textual.screen import ModalScreen
|
||||
from textual.widgets import Button, Static
|
||||
from textual.containers import Container, Vertical
|
||||
from textual import on
|
||||
|
||||
class ErrorModal(ModalScreen):
|
||||
"""Modal dialog for displaying errors with retry capability."""
|
||||
|
||||
def __init__(self, message: str, retry_action: callable = None):
|
||||
super().__init__()
|
||||
self.message = message
|
||||
self.retry_action = retry_action
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
with Vertical(id="error-dialog"):
|
||||
yield Static(f"⚠️ {self.message}", id="error-message")
|
||||
with Container(id="error-buttons"):
|
||||
if self.retry_action:
|
||||
yield Button("Retry", variant="error", id="retry-btn")
|
||||
yield Button("Dismiss", variant="primary", id="dismiss-btn")
|
||||
|
||||
@on(Button.Pressed, "#retry-btn")
|
||||
def on_retry(self):
|
||||
if self.retry_action:
|
||||
self.dismiss()
|
||||
self.retry_action()
|
||||
|
||||
@on(Button.Pressed, "#dismiss-btn")
|
||||
def on_dismiss(self):
|
||||
self.dismiss()
|
||||
18
tui/widgets/loading.py
Normal file
18
tui/widgets/loading.py
Normal file
@@ -0,0 +1,18 @@
|
||||
"""
|
||||
Loading spinner components for TUI.
|
||||
"""
|
||||
from textual.widgets import Static
|
||||
from rich.spinner import Spinner
|
||||
|
||||
class LoadingSpinner(Static):
|
||||
"""Animated loading spinner component."""
|
||||
|
||||
def __init__(self, text: str = "Loading...", spinner: str = "dots") -> None:
|
||||
super().__init__()
|
||||
self.spinner = Spinner(spinner, text=text)
|
||||
|
||||
def on_mount(self) -> None:
|
||||
self.set_interval(0.1, self.update_spinner)
|
||||
|
||||
def update_spinner(self) -> None:
|
||||
self.update(self.spinner)
|
||||
Reference in New Issue
Block a user