mirror of
https://github.com/sstent/GarminSync.git
synced 2026-01-25 16:42:20 +00:00
working again
This commit is contained in:
10
Dockerfile
10
Dockerfile
@@ -16,14 +16,16 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|||||||
# Copy and install Python dependencies
|
# Copy and install Python dependencies
|
||||||
COPY requirements.txt .
|
COPY requirements.txt .
|
||||||
RUN pip install --no-cache-dir --upgrade pip && \
|
RUN pip install --no-cache-dir --upgrade pip && \
|
||||||
pip install --no-cache-dir -r requirements.txt
|
pip install --no-cache-dir -r requirements.txt && \
|
||||||
|
pip install --no-cache-dir alembic
|
||||||
# Copy application code
|
# Copy application code
|
||||||
COPY garminsync/ ./garminsync/
|
COPY garminsync/ ./garminsync/
|
||||||
|
COPY migrations/ ./migrations/
|
||||||
|
COPY entrypoint.sh .
|
||||||
|
RUN chmod +x entrypoint.sh
|
||||||
|
|
||||||
# Create data directory
|
# Create data directory
|
||||||
RUN mkdir -p /app/data
|
RUN mkdir -p /app/data
|
||||||
|
|
||||||
# Set environment variables from .env file
|
# Set environment variables from .env file
|
||||||
ENV ENV_FILE=/app/.env
|
ENV ENV_FILE=/app/.env
|
||||||
ENV DATA_DIR=/app/data
|
ENV DATA_DIR=/app/data
|
||||||
@@ -32,5 +34,5 @@ ENV DATA_DIR=/app/data
|
|||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
|
||||||
# Update entrypoint to support daemon mode
|
# Update entrypoint to support daemon mode
|
||||||
ENTRYPOINT ["python", "-m", "garminsync.cli"]
|
ENTRYPOINT ["./entrypoint.sh"]
|
||||||
CMD ["--help"]
|
CMD ["--help"]
|
||||||
|
|||||||
203
plan.md
203
plan.md
@@ -1,203 +0,0 @@
|
|||||||
# GarminSync Fixes and Updated Requirements
|
|
||||||
|
|
||||||
## Primary Issue: Dependency Conflicts
|
|
||||||
|
|
||||||
The main error you're encountering is a dependency conflict between `pydantic` and `garth` (a dependency of `garminconnect`). Here's the solution:
|
|
||||||
|
|
||||||
### Updated requirements.txt
|
|
||||||
```
|
|
||||||
typer==0.9.0
|
|
||||||
click==8.1.7
|
|
||||||
python-dotenv==1.0.0
|
|
||||||
garminconnect==0.2.29
|
|
||||||
sqlalchemy==2.0.23
|
|
||||||
tqdm==4.66.1
|
|
||||||
fastapi==0.104.1
|
|
||||||
uvicorn[standard]==0.24.0
|
|
||||||
apscheduler==3.10.4
|
|
||||||
pydantic>=2.0.0,<2.5.0
|
|
||||||
jinja2==3.1.2
|
|
||||||
python-multipart==0.0.6
|
|
||||||
aiofiles==23.2.1
|
|
||||||
```
|
|
||||||
|
|
||||||
**Key Change**: Changed `pydantic==2.5.0` to `pydantic>=2.0.0,<2.5.0` to avoid the compatibility issue with `garth`.
|
|
||||||
|
|
||||||
## Code Issues Found and Fixes
|
|
||||||
|
|
||||||
### 1. Missing utils.py File
|
|
||||||
Your `daemon.py` imports `from .utils import logger` but this file doesn't exist.
|
|
||||||
|
|
||||||
**Fix**: Create `garminsync/utils.py`:
|
|
||||||
```python
|
|
||||||
import logging
|
|
||||||
|
|
||||||
# Configure logging
|
|
||||||
logging.basicConfig(
|
|
||||||
level=logging.INFO,
|
|
||||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
||||||
)
|
|
||||||
|
|
||||||
logger = logging.getLogger('garminsync')
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Daemon.py Import Issues
|
|
||||||
The `daemon.py` file has several import and method call issues.
|
|
||||||
|
|
||||||
**Fix for garminsync/daemon.py** (line 56-75):
|
|
||||||
```python
|
|
||||||
def sync_and_download(self):
|
|
||||||
"""Scheduled job function"""
|
|
||||||
try:
|
|
||||||
self.log_operation("sync", "started")
|
|
||||||
|
|
||||||
# Import here to avoid circular imports
|
|
||||||
from .garmin import GarminClient
|
|
||||||
from .database import sync_database
|
|
||||||
|
|
||||||
# Perform sync and download
|
|
||||||
client = GarminClient()
|
|
||||||
|
|
||||||
# Sync database first
|
|
||||||
sync_database(client)
|
|
||||||
|
|
||||||
# Download missing activities
|
|
||||||
downloaded_count = 0
|
|
||||||
session = get_session()
|
|
||||||
missing_activities = session.query(Activity).filter_by(downloaded=False).all()
|
|
||||||
|
|
||||||
for activity in missing_activities:
|
|
||||||
try:
|
|
||||||
# Use the correct method name
|
|
||||||
fit_data = client.download_activity_fit(activity.activity_id)
|
|
||||||
|
|
||||||
# Save the file
|
|
||||||
import os
|
|
||||||
from pathlib import Path
|
|
||||||
data_dir = Path(os.getenv("DATA_DIR", "data"))
|
|
||||||
data_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
timestamp = activity.start_time.replace(":", "-").replace(" ", "_")
|
|
||||||
filename = f"activity_{activity.activity_id}_{timestamp}.fit"
|
|
||||||
filepath = data_dir / filename
|
|
||||||
|
|
||||||
with open(filepath, "wb") as f:
|
|
||||||
f.write(fit_data)
|
|
||||||
|
|
||||||
activity.filename = str(filepath)
|
|
||||||
activity.downloaded = True
|
|
||||||
activity.last_sync = datetime.now().isoformat()
|
|
||||||
downloaded_count += 1
|
|
||||||
session.commit()
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Failed to download activity {activity.activity_id}: {e}")
|
|
||||||
session.rollback()
|
|
||||||
|
|
||||||
session.close()
|
|
||||||
self.log_operation("sync", "success",
|
|
||||||
f"Downloaded {downloaded_count} new activities")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Sync failed: {e}")
|
|
||||||
self.log_operation("sync", "error", str(e))
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Missing created_at Field in Database Sync
|
|
||||||
The `sync_database` function in `database.py` doesn't set the `created_at` field.
|
|
||||||
|
|
||||||
**Fix for garminsync/database.py** (line 64-75):
|
|
||||||
```python
|
|
||||||
def sync_database(garmin_client):
|
|
||||||
"""Sync local database with Garmin Connect activities"""
|
|
||||||
from datetime import datetime
|
|
||||||
session = get_session()
|
|
||||||
try:
|
|
||||||
# Fetch activities from Garmin Connect
|
|
||||||
activities = garmin_client.get_activities(0, 1000)
|
|
||||||
|
|
||||||
# Process activities and update database
|
|
||||||
for activity in activities:
|
|
||||||
activity_id = activity["activityId"]
|
|
||||||
start_time = activity["startTimeLocal"]
|
|
||||||
|
|
||||||
# Check if activity exists in database
|
|
||||||
existing = session.query(Activity).filter_by(activity_id=activity_id).first()
|
|
||||||
if not existing:
|
|
||||||
new_activity = Activity(
|
|
||||||
activity_id=activity_id,
|
|
||||||
start_time=start_time,
|
|
||||||
downloaded=False,
|
|
||||||
created_at=datetime.now().isoformat(), # Add this line
|
|
||||||
last_sync=datetime.now().isoformat()
|
|
||||||
)
|
|
||||||
session.add(new_activity)
|
|
||||||
|
|
||||||
session.commit()
|
|
||||||
except SQLAlchemyError as e:
|
|
||||||
session.rollback()
|
|
||||||
raise e
|
|
||||||
finally:
|
|
||||||
session.close()
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Add Missing created_at Field to Database Model
|
|
||||||
The `Activity` model is missing the `created_at` field that's used in the daemon.
|
|
||||||
|
|
||||||
**Fix for garminsync/database.py** (line 12):
|
|
||||||
```python
|
|
||||||
class Activity(Base):
|
|
||||||
__tablename__ = 'activities'
|
|
||||||
|
|
||||||
activity_id = Column(Integer, primary_key=True)
|
|
||||||
start_time = Column(String, nullable=False)
|
|
||||||
filename = Column(String, unique=True, nullable=True)
|
|
||||||
downloaded = Column(Boolean, default=False, nullable=False)
|
|
||||||
created_at = Column(String, nullable=False) # Add this line
|
|
||||||
last_sync = Column(String, nullable=True) # ISO timestamp of last sync
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. JavaScript Function Missing in Dashboard
|
|
||||||
The dashboard template calls `toggleDaemon()` but this function doesn't exist in the JavaScript.
|
|
||||||
|
|
||||||
**Fix for garminsync/web/static/app.js** (add this function):
|
|
||||||
```javascript
|
|
||||||
async function toggleDaemon() {
|
|
||||||
// TODO: Implement daemon toggle functionality
|
|
||||||
alert('Daemon toggle functionality not yet implemented');
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Testing the Fixes
|
|
||||||
|
|
||||||
After applying these fixes:
|
|
||||||
|
|
||||||
1. **Rebuild the Docker image**:
|
|
||||||
```bash
|
|
||||||
docker build -t garminsync .
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Test the daemon mode**:
|
|
||||||
```bash
|
|
||||||
docker run -d --env-file .env -v $(pwd)/data:/app/data -p 8080:8080 garminsync daemon --start
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Check the logs**:
|
|
||||||
```bash
|
|
||||||
docker logs <container_id>
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **Access the web UI**:
|
|
||||||
Open http://localhost:8080 in your browser
|
|
||||||
|
|
||||||
## Additional Recommendations
|
|
||||||
|
|
||||||
1. **Add error handling for missing directories**: The daemon should create the data directory if it doesn't exist.
|
|
||||||
|
|
||||||
2. **Improve logging**: Add more detailed logging throughout the application.
|
|
||||||
|
|
||||||
3. **Add health checks**: Implement health check endpoints for the daemon.
|
|
||||||
|
|
||||||
4. **Database migrations**: Consider adding database migration support for schema changes.
|
|
||||||
|
|
||||||
The primary fix for your immediate issue is updating the `pydantic` version constraint in `requirements.txt`. The other fixes address various code quality and functionality issues I found during the review.
|
|
||||||
@@ -1,173 +0,0 @@
|
|||||||
# GarminSync UI Redesign Implementation Summary
|
|
||||||
|
|
||||||
This document summarizes the implementation of the UI redesign for GarminSync as specified in the ui_plan.md file.
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
The UI redesign transformed the existing bootstrap-based interface into a modern, clean design with two main pages: Home (dashboard with statistics and sync controls) and Activities (data table view).
|
|
||||||
|
|
||||||
## Changes Made
|
|
||||||
|
|
||||||
### 1. Backend API Enhancements
|
|
||||||
|
|
||||||
#### Database Model Updates
|
|
||||||
- Enhanced the `Activity` model in `garminsync/database.py` with new fields:
|
|
||||||
- `activity_type` (String)
|
|
||||||
- `duration` (Integer, seconds)
|
|
||||||
- `distance` (Float, meters)
|
|
||||||
- `max_heart_rate` (Integer)
|
|
||||||
- `avg_power` (Float)
|
|
||||||
- `calories` (Integer)
|
|
||||||
|
|
||||||
#### New API Endpoints
|
|
||||||
Added the following endpoints in `garminsync/web/routes.py`:
|
|
||||||
- `GET /api/activities` - Get paginated activities with filtering
|
|
||||||
- `GET /api/activities/{activity_id}` - Get detailed activity information
|
|
||||||
- `GET /api/dashboard/stats` - Get comprehensive dashboard statistics
|
|
||||||
|
|
||||||
### 2. Frontend Architecture Redesign
|
|
||||||
|
|
||||||
#### CSS Restructuring
|
|
||||||
Created new CSS files in `garminsync/web/static/`:
|
|
||||||
- `style.css` - Core styling with CSS variables and modern layout
|
|
||||||
- `components.css` - Advanced component styling (tables, buttons, etc.)
|
|
||||||
- `responsive.css` - Mobile-first responsive design
|
|
||||||
|
|
||||||
Key features:
|
|
||||||
- Replaced Bootstrap with custom CSS using CSS Grid/Flexbox
|
|
||||||
- Implemented CSS variables for consistent theming
|
|
||||||
- Created modern card components with shadows and rounded corners
|
|
||||||
- Added responsive design with mobile-first approach
|
|
||||||
|
|
||||||
#### JavaScript Architecture
|
|
||||||
Created new JavaScript files:
|
|
||||||
- `navigation.js` - Dynamic navigation component
|
|
||||||
- `utils.js` - Common utility functions
|
|
||||||
- `home.js` - Home page controller
|
|
||||||
- `activities.js` - Activities page controller
|
|
||||||
|
|
||||||
Updated existing files:
|
|
||||||
- `logs.js` - Refactored to use new styling and components
|
|
||||||
- `app.js` - Deprecated (functionality moved to new files)
|
|
||||||
- `charts.js` - Deprecated (chart functionality removed)
|
|
||||||
|
|
||||||
### 3. Template Redesign
|
|
||||||
|
|
||||||
#### Base Template
|
|
||||||
Updated `garminsync/web/templates/base.html`:
|
|
||||||
- Removed Bootstrap dependencies
|
|
||||||
- Added links to new CSS files
|
|
||||||
- Updated script loading
|
|
||||||
|
|
||||||
#### Home Page
|
|
||||||
Redesigned `garminsync/web/templates/dashboard.html`:
|
|
||||||
- Implemented new layout with sidebar and main content area
|
|
||||||
- Added sync button with status indicator
|
|
||||||
- Created statistics display with clean card layout
|
|
||||||
- Added log data display area
|
|
||||||
|
|
||||||
#### Activities Page
|
|
||||||
Created `garminsync/web/templates/activities.html`:
|
|
||||||
- Implemented data table view with all activity details
|
|
||||||
- Added pagination controls
|
|
||||||
- Used consistent styling with other pages
|
|
||||||
|
|
||||||
#### Other Templates
|
|
||||||
Updated `garminsync/web/templates/logs.html` and `garminsync/web/templates/config.html`:
|
|
||||||
- Applied new styling and components
|
|
||||||
- Maintained existing functionality
|
|
||||||
|
|
||||||
### 4. Application Updates
|
|
||||||
|
|
||||||
#### Route Configuration
|
|
||||||
Updated `garminsync/web/app.py`:
|
|
||||||
- Added new route for activities page
|
|
||||||
|
|
||||||
#### Documentation
|
|
||||||
Updated `README.md`:
|
|
||||||
- Added Activities to Web Interface features list
|
|
||||||
- Updated Web API Endpoints section with new endpoints
|
|
||||||
|
|
||||||
### 5. Migration and Testing
|
|
||||||
|
|
||||||
#### Migration Script
|
|
||||||
Created `garminsync/migrate_activities.py`:
|
|
||||||
- Script to populate new activity fields from Garmin API
|
|
||||||
- Handles error cases and provides progress feedback
|
|
||||||
|
|
||||||
#### Test Script
|
|
||||||
Created `garminsync/web/test_ui.py`:
|
|
||||||
- Tests all new UI endpoints
|
|
||||||
- Verifies API endpoints are working correctly
|
|
||||||
|
|
||||||
## Key Improvements
|
|
||||||
|
|
||||||
1. **Modern Design**: Clean, contemporary interface with consistent styling
|
|
||||||
2. **Improved Navigation**: Tab-based navigation between main pages
|
|
||||||
3. **Better Data Presentation**: Enhanced tables with alternating row colors and hover effects
|
|
||||||
4. **Responsive Layout**: Mobile-friendly design that works on all screen sizes
|
|
||||||
5. **Performance**: Removed heavy Bootstrap dependency for lighter, faster loading
|
|
||||||
6. **Maintainability**: Modular JavaScript architecture with clear separation of concerns
|
|
||||||
|
|
||||||
## Files Created
|
|
||||||
|
|
||||||
- `garminsync/web/static/style.css`
|
|
||||||
- `garminsync/web/static/components.css`
|
|
||||||
- `garminsync/web/static/responsive.css`
|
|
||||||
- `garminsync/web/static/navigation.js`
|
|
||||||
- `garminsync/web/static/utils.js`
|
|
||||||
- `garminsync/web/static/home.js`
|
|
||||||
- `garminsync/web/static/activities.js`
|
|
||||||
- `garminsync/web/templates/activities.html`
|
|
||||||
- `garminsync/migrate_activities.py`
|
|
||||||
- `garminsync/web/test_ui.py`
|
|
||||||
- `ui_implementation_summary.md`
|
|
||||||
|
|
||||||
## Files Modified
|
|
||||||
|
|
||||||
- `garminsync/database.py`
|
|
||||||
- `garminsync/web/routes.py`
|
|
||||||
- `garminsync/web/templates/base.html`
|
|
||||||
- `garminsync/web/templates/dashboard.html`
|
|
||||||
- `garminsync/web/templates/logs.html`
|
|
||||||
- `garminsync/web/templates/config.html`
|
|
||||||
- `garminsync/web/app.py`
|
|
||||||
- `garminsync/web/static/logs.js`
|
|
||||||
- `garminsync/web/static/app.js`
|
|
||||||
- `garminsync/web/static/charts.js`
|
|
||||||
- `README.md`
|
|
||||||
|
|
||||||
## Files Deprecated
|
|
||||||
|
|
||||||
- `garminsync/web/static/app.js` (functionality moved)
|
|
||||||
- `garminsync/web/static/charts.js` (chart functionality removed)
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
The implementation includes a test script (`garminsync/web/test_ui.py`) that verifies:
|
|
||||||
- All UI endpoints are accessible
|
|
||||||
- New API endpoints return expected responses
|
|
||||||
- Basic functionality is working correctly
|
|
||||||
|
|
||||||
## Migration
|
|
||||||
|
|
||||||
The migration script (`garminsync/migrate_activities.py`) can be run to:
|
|
||||||
- Populate new activity fields from Garmin API
|
|
||||||
- Update existing activities with detailed information
|
|
||||||
- Provide progress feedback during migration
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
After implementing these changes, the GarminSync web interface provides:
|
|
||||||
|
|
||||||
1. **Home Page**: Dashboard with sync controls, statistics, and log display
|
|
||||||
2. **Activities Page**: Comprehensive table view of all activities with filtering and pagination
|
|
||||||
3. **Logs Page**: Filterable and paginated sync logs
|
|
||||||
4. **Configuration Page**: Daemon settings and status management
|
|
||||||
|
|
||||||
All pages feature:
|
|
||||||
- Modern, clean design
|
|
||||||
- Responsive layout for all device sizes
|
|
||||||
- Consistent navigation
|
|
||||||
- Real-time updates
|
|
||||||
- Enhanced data presentation
|
|
||||||
687
ui_plan.md
687
ui_plan.md
@@ -1,687 +0,0 @@
|
|||||||
# GarminSync UI Redesign Implementation Plan
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
Transform the existing GarminSync web interface from the current bootstrap-based UI to a modern, clean design matching the provided mockups. The target design shows two main pages: Home (dashboard with statistics and sync controls) and Activities (data table view).
|
|
||||||
|
|
||||||
## Current State Analysis
|
|
||||||
|
|
||||||
### Existing Structure
|
|
||||||
- **Backend**: FastAPI with SQLAlchemy, scheduled daemon
|
|
||||||
- **Frontend**: Bootstrap 5 + jQuery, basic dashboard
|
|
||||||
- **Database**: SQLite with Activity, DaemonConfig, SyncLog models
|
|
||||||
- **Templates**: Jinja2 templates in `garminsync/web/templates/`
|
|
||||||
- **Static Assets**: Basic CSS/JS in `garminsync/web/static/`
|
|
||||||
|
|
||||||
### Current Pages
|
|
||||||
- Dashboard: Basic stats, daemon controls, logs
|
|
||||||
- Configuration: Daemon settings, cron scheduling
|
|
||||||
- Logs: Paginated sync logs with filters
|
|
||||||
|
|
||||||
## Target Design Requirements
|
|
||||||
|
|
||||||
### Home Page Layout
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────────────────────────┐
|
|
||||||
│ Navigation: [Home] [Activities] │
|
|
||||||
├─────────────────────────────────────────────────────────┤
|
|
||||||
│ Left Sidebar (25%) │ Right Content Area (75%) │
|
|
||||||
│ ┌─────────────────┐ │ ┌─────────────────────────────┐ │
|
|
||||||
│ │ Sync Now │ │ │ │ │
|
|
||||||
│ │ (Blue Button) │ │ │ Log Data Display │ │
|
|
||||||
│ └─────────────────┘ │ │ │ │
|
|
||||||
│ ┌─────────────────┐ │ │ │ │
|
|
||||||
│ │ Statistics │ │ │ │ │
|
|
||||||
│ │ Total: 852 │ │ │ │ │
|
|
||||||
│ │ Downloaded: 838 │ │ │ │ │
|
|
||||||
│ │ Missing: 14 │ │ │ │ │
|
|
||||||
│ └─────────────────┘ │ └─────────────────────────────┘ │
|
|
||||||
└─────────────────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
### Activities Page Layout
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────────────────────────┐
|
|
||||||
│ Navigation: [Home] [Activities] │
|
|
||||||
├─────────────────────────────────────────────────────────┤
|
|
||||||
│ │
|
|
||||||
│ ┌─────────────────────────────────────────────────────┐ │
|
|
||||||
│ │ Date │Activity Type│Duration│Distance│Max HR│Power │ │
|
|
||||||
│ │────────────────────────────────────────────────────│ │
|
|
||||||
│ │ │ │ │ │ │ │ │
|
|
||||||
│ │ │ │ │ │ │ │ │
|
|
||||||
│ └─────────────────────────────────────────────────────┘ │
|
|
||||||
└─────────────────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
## Implementation Plan
|
|
||||||
|
|
||||||
### Phase 1: Backend API Enhancements
|
|
||||||
|
|
||||||
#### 1.1 New API Endpoints
|
|
||||||
**File: `garminsync/web/routes.py`**
|
|
||||||
|
|
||||||
Add missing endpoints:
|
|
||||||
```python
|
|
||||||
@router.get("/activities")
|
|
||||||
async def get_activities(
|
|
||||||
page: int = 1,
|
|
||||||
per_page: int = 50,
|
|
||||||
activity_type: str = None,
|
|
||||||
date_from: str = None,
|
|
||||||
date_to: str = None
|
|
||||||
):
|
|
||||||
"""Get paginated activities with filtering"""
|
|
||||||
|
|
||||||
@router.get("/activities/{activity_id}")
|
|
||||||
async def get_activity_details(activity_id: int):
|
|
||||||
"""Get detailed activity information"""
|
|
||||||
|
|
||||||
@router.get("/dashboard/stats")
|
|
||||||
async def get_dashboard_stats():
|
|
||||||
"""Get comprehensive dashboard statistics"""
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 1.2 Database Model Enhancements
|
|
||||||
**File: `garminsync/database.py`**
|
|
||||||
|
|
||||||
Enhance Activity model:
|
|
||||||
```python
|
|
||||||
class Activity(Base):
|
|
||||||
__tablename__ = 'activities'
|
|
||||||
|
|
||||||
activity_id = Column(Integer, primary_key=True)
|
|
||||||
start_time = Column(String, nullable=False)
|
|
||||||
activity_type = Column(String, nullable=True) # NEW
|
|
||||||
duration = Column(Integer, nullable=True) # NEW (seconds)
|
|
||||||
distance = Column(Float, nullable=True) # NEW (meters)
|
|
||||||
max_heart_rate = Column(Integer, nullable=True) # NEW
|
|
||||||
avg_power = Column(Float, nullable=True) # NEW
|
|
||||||
calories = Column(Integer, nullable=True) # NEW
|
|
||||||
filename = Column(String, unique=True, nullable=True)
|
|
||||||
downloaded = Column(Boolean, default=False, nullable=False)
|
|
||||||
created_at = Column(String, nullable=False)
|
|
||||||
last_sync = Column(String, nullable=True)
|
|
||||||
```
|
|
||||||
|
|
||||||
Add migration function to populate new fields from Garmin API.
|
|
||||||
|
|
||||||
### Phase 2: Frontend Architecture Redesign
|
|
||||||
|
|
||||||
#### 2.1 Modern CSS Framework
|
|
||||||
**File: `garminsync/web/static/style.css`**
|
|
||||||
|
|
||||||
Replace Bootstrap with custom CSS using modern techniques:
|
|
||||||
```css
|
|
||||||
/* CSS Variables for consistent theming */
|
|
||||||
:root {
|
|
||||||
--primary-color: #007bff;
|
|
||||||
--secondary-color: #6c757d;
|
|
||||||
--success-color: #28a745;
|
|
||||||
--danger-color: #dc3545;
|
|
||||||
--light-gray: #f8f9fa;
|
|
||||||
--dark-gray: #343a40;
|
|
||||||
--border-radius: 8px;
|
|
||||||
--box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* CSS Grid Layout System */
|
|
||||||
.container {
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 0 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.layout-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 300px 1fr;
|
|
||||||
gap: 20px;
|
|
||||||
min-height: calc(100vh - 60px);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Modern Card Components */
|
|
||||||
.card {
|
|
||||||
background: white;
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
box-shadow: var(--box-shadow);
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive Design */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.layout-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 2.2 Navigation Component
|
|
||||||
**File: `garminsync/web/static/navigation.js`**
|
|
||||||
|
|
||||||
Create dynamic navigation:
|
|
||||||
```javascript
|
|
||||||
class Navigation {
|
|
||||||
constructor() {
|
|
||||||
this.currentPage = this.getCurrentPage();
|
|
||||||
this.render();
|
|
||||||
}
|
|
||||||
|
|
||||||
getCurrentPage() {
|
|
||||||
return window.location.pathname === '/activities' ? 'activities' : 'home';
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const nav = document.querySelector('.navigation');
|
|
||||||
nav.innerHTML = this.getNavigationHTML();
|
|
||||||
this.attachEventListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
getNavigationHTML() {
|
|
||||||
return `
|
|
||||||
<nav class="nav-tabs">
|
|
||||||
<button class="nav-tab ${this.currentPage === 'home' ? 'active' : ''}"
|
|
||||||
data-page="home">Home</button>
|
|
||||||
<button class="nav-tab ${this.currentPage === 'activities' ? 'active' : ''}"
|
|
||||||
data-page="activities">Activities</button>
|
|
||||||
</nav>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Phase 3: Home Page Implementation
|
|
||||||
|
|
||||||
#### 3.1 Home Page Template Redesign
|
|
||||||
**File: `garminsync/web/templates/dashboard.html`**
|
|
||||||
|
|
||||||
```html
|
|
||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="container">
|
|
||||||
<div class="navigation"></div>
|
|
||||||
|
|
||||||
<div class="layout-grid">
|
|
||||||
<!-- Left Sidebar -->
|
|
||||||
<div class="sidebar">
|
|
||||||
<div class="card sync-card">
|
|
||||||
<button id="sync-now-btn" class="btn btn-primary btn-large">
|
|
||||||
<i class="icon-sync"></i>
|
|
||||||
Sync Now
|
|
||||||
</button>
|
|
||||||
<div class="sync-status" id="sync-status">
|
|
||||||
Ready to sync
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card statistics-card">
|
|
||||||
<h3>Statistics</h3>
|
|
||||||
<div class="stat-item">
|
|
||||||
<label>Total Activities:</label>
|
|
||||||
<span id="total-activities">{{stats.total}}</span>
|
|
||||||
</div>
|
|
||||||
<div class="stat-item">
|
|
||||||
<label>Downloaded:</label>
|
|
||||||
<span id="downloaded-activities">{{stats.downloaded}}</span>
|
|
||||||
</div>
|
|
||||||
<div class="stat-item">
|
|
||||||
<label>Missing:</label>
|
|
||||||
<span id="missing-activities">{{stats.missing}}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Right Content Area -->
|
|
||||||
<div class="main-content">
|
|
||||||
<div class="card log-display">
|
|
||||||
<div class="card-header">
|
|
||||||
<h3>Log Data</h3>
|
|
||||||
</div>
|
|
||||||
<div class="log-content" id="log-content">
|
|
||||||
<!-- Real-time log updates will appear here -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 3.2 Home Page JavaScript Controller
|
|
||||||
**File: `garminsync/web/static/home.js`**
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
class HomePage {
|
|
||||||
constructor() {
|
|
||||||
this.logSocket = null;
|
|
||||||
this.statsRefreshInterval = null;
|
|
||||||
this.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
|
||||||
this.attachEventListeners();
|
|
||||||
this.setupRealTimeUpdates();
|
|
||||||
this.loadInitialData();
|
|
||||||
}
|
|
||||||
|
|
||||||
attachEventListeners() {
|
|
||||||
document.getElementById('sync-now-btn').addEventListener('click',
|
|
||||||
() => this.triggerSync());
|
|
||||||
}
|
|
||||||
|
|
||||||
async triggerSync() {
|
|
||||||
const btn = document.getElementById('sync-now-btn');
|
|
||||||
const status = document.getElementById('sync-status');
|
|
||||||
|
|
||||||
btn.disabled = true;
|
|
||||||
btn.innerHTML = '<i class="icon-loading"></i> Syncing...';
|
|
||||||
status.textContent = 'Sync in progress...';
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/sync/trigger', {method: 'POST'});
|
|
||||||
const result = await response.json();
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
status.textContent = 'Sync completed successfully';
|
|
||||||
this.updateStats();
|
|
||||||
} else {
|
|
||||||
throw new Error(result.detail || 'Sync failed');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
status.textContent = `Sync failed: ${error.message}`;
|
|
||||||
} finally {
|
|
||||||
btn.disabled = false;
|
|
||||||
btn.innerHTML = '<i class="icon-sync"></i> Sync Now';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setupRealTimeUpdates() {
|
|
||||||
// Poll for log updates every 5 seconds during active operations
|
|
||||||
this.startLogPolling();
|
|
||||||
|
|
||||||
// Update stats every 30 seconds
|
|
||||||
this.statsRefreshInterval = setInterval(() => {
|
|
||||||
this.updateStats();
|
|
||||||
}, 30000);
|
|
||||||
}
|
|
||||||
|
|
||||||
async startLogPolling() {
|
|
||||||
// Implementation for real-time log updates
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateStats() {
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/activities/stats');
|
|
||||||
const stats = await response.json();
|
|
||||||
|
|
||||||
document.getElementById('total-activities').textContent = stats.total;
|
|
||||||
document.getElementById('downloaded-activities').textContent = stats.downloaded;
|
|
||||||
document.getElementById('missing-activities').textContent = stats.missing;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to update stats:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Phase 4: Activities Page Implementation
|
|
||||||
|
|
||||||
#### 4.1 Activities Page Template
|
|
||||||
**File: `garminsync/web/templates/activities.html`**
|
|
||||||
|
|
||||||
```html
|
|
||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="container">
|
|
||||||
<div class="navigation"></div>
|
|
||||||
|
|
||||||
<div class="activities-container">
|
|
||||||
<div class="card activities-table-card">
|
|
||||||
<div class="table-container">
|
|
||||||
<table class="activities-table" id="activities-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Date</th>
|
|
||||||
<th>Activity Type</th>
|
|
||||||
<th>Duration</th>
|
|
||||||
<th>Distance</th>
|
|
||||||
<th>Max HR</th>
|
|
||||||
<th>Power</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="activities-tbody">
|
|
||||||
<!-- Data populated by JavaScript -->
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="pagination-container">
|
|
||||||
<div class="pagination" id="pagination">
|
|
||||||
<!-- Pagination controls -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 4.2 Activities Table Controller
|
|
||||||
**File: `garminsync/web/static/activities.js`**
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
class ActivitiesPage {
|
|
||||||
constructor() {
|
|
||||||
this.currentPage = 1;
|
|
||||||
this.pageSize = 25;
|
|
||||||
this.totalPages = 1;
|
|
||||||
this.activities = [];
|
|
||||||
this.filters = {};
|
|
||||||
this.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
|
||||||
this.loadActivities();
|
|
||||||
this.setupEventListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadActivities() {
|
|
||||||
try {
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
page: this.currentPage,
|
|
||||||
per_page: this.pageSize,
|
|
||||||
...this.filters
|
|
||||||
});
|
|
||||||
|
|
||||||
const response = await fetch(`/api/activities?${params}`);
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
this.activities = data.activities;
|
|
||||||
this.totalPages = Math.ceil(data.total / this.pageSize);
|
|
||||||
|
|
||||||
this.renderTable();
|
|
||||||
this.renderPagination();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to load activities:', error);
|
|
||||||
this.showError('Failed to load activities');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
renderTable() {
|
|
||||||
const tbody = document.getElementById('activities-tbody');
|
|
||||||
tbody.innerHTML = '';
|
|
||||||
|
|
||||||
this.activities.forEach((activity, index) => {
|
|
||||||
const row = this.createTableRow(activity, index);
|
|
||||||
tbody.appendChild(row);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
createTableRow(activity, index) {
|
|
||||||
const row = document.createElement('tr');
|
|
||||||
row.className = index % 2 === 0 ? 'row-even' : 'row-odd';
|
|
||||||
|
|
||||||
row.innerHTML = `
|
|
||||||
<td>${this.formatDate(activity.start_time)}</td>
|
|
||||||
<td>${activity.activity_type || '-'}</td>
|
|
||||||
<td>${this.formatDuration(activity.duration)}</td>
|
|
||||||
<td>${this.formatDistance(activity.distance)}</td>
|
|
||||||
<td>${activity.max_heart_rate || '-'}</td>
|
|
||||||
<td>${this.formatPower(activity.avg_power)}</td>
|
|
||||||
`;
|
|
||||||
|
|
||||||
return row;
|
|
||||||
}
|
|
||||||
|
|
||||||
formatDate(dateStr) {
|
|
||||||
return new Date(dateStr).toLocaleDateString();
|
|
||||||
}
|
|
||||||
|
|
||||||
formatDuration(seconds) {
|
|
||||||
if (!seconds) return '-';
|
|
||||||
const hours = Math.floor(seconds / 3600);
|
|
||||||
const minutes = Math.floor((seconds % 3600) / 60);
|
|
||||||
return `${hours}:${minutes.toString().padStart(2, '0')}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
formatDistance(meters) {
|
|
||||||
if (!meters) return '-';
|
|
||||||
return `${(meters / 1000).toFixed(1)} km`;
|
|
||||||
}
|
|
||||||
|
|
||||||
formatPower(watts) {
|
|
||||||
return watts ? `${Math.round(watts)}W` : '-';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Phase 5: Styling and Visual Polish
|
|
||||||
|
|
||||||
#### 5.1 Advanced CSS Styling
|
|
||||||
**File: `garminsync/web/static/components.css`**
|
|
||||||
|
|
||||||
```css
|
|
||||||
/* Table Styling */
|
|
||||||
.activities-table {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.activities-table thead {
|
|
||||||
background-color: #000;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.activities-table th {
|
|
||||||
padding: 12px 16px;
|
|
||||||
text-align: left;
|
|
||||||
font-weight: 600;
|
|
||||||
border-right: 1px solid #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.activities-table td {
|
|
||||||
padding: 12px 16px;
|
|
||||||
border-bottom: 1px solid #eee;
|
|
||||||
}
|
|
||||||
|
|
||||||
.activities-table .row-even {
|
|
||||||
background-color: #f8f9fa;
|
|
||||||
}
|
|
||||||
|
|
||||||
.activities-table .row-odd {
|
|
||||||
background-color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Sync Button Styling */
|
|
||||||
.btn-primary.btn-large {
|
|
||||||
width: 100%;
|
|
||||||
padding: 15px;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
background: linear-gradient(135deg, #007bff 0%, #0056b3 100%);
|
|
||||||
border: none;
|
|
||||||
color: white;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary.btn-large:hover {
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 4px 12px rgba(0,123,255,0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary.btn-large:disabled {
|
|
||||||
opacity: 0.6;
|
|
||||||
cursor: not-allowed;
|
|
||||||
transform: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Statistics Card */
|
|
||||||
.statistics-card .stat-item {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
padding: 8px 0;
|
|
||||||
border-bottom: 1px solid #eee;
|
|
||||||
}
|
|
||||||
|
|
||||||
.statistics-card .stat-item:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.statistics-card label {
|
|
||||||
font-weight: 500;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
.statistics-card span {
|
|
||||||
font-weight: 600;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 5.2 Responsive Design
|
|
||||||
**File: `garminsync/web/static/responsive.css`**
|
|
||||||
|
|
||||||
```css
|
|
||||||
/* Mobile-first responsive design */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.layout-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
gap: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar {
|
|
||||||
order: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-content {
|
|
||||||
order: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.activities-table {
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.activities-table th,
|
|
||||||
.activities-table td {
|
|
||||||
padding: 8px 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
|
||||||
.activities-table {
|
|
||||||
display: block;
|
|
||||||
overflow-x: auto;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Phase 6: Integration and Testing
|
|
||||||
|
|
||||||
#### 6.1 Updated Base Template
|
|
||||||
**File: `garminsync/web/templates/base.html`**
|
|
||||||
|
|
||||||
```html
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>GarminSync</title>
|
|
||||||
<link href="/static/style.css" rel="stylesheet">
|
|
||||||
<link href="/static/components.css" rel="stylesheet">
|
|
||||||
<link href="/static/responsive.css" rel="stylesheet">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
{% block content %}{% endblock %}
|
|
||||||
|
|
||||||
<script src="/static/navigation.js"></script>
|
|
||||||
<script src="/static/utils.js"></script>
|
|
||||||
|
|
||||||
{% block page_scripts %}{% endblock %}
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 6.2 App Router Updates
|
|
||||||
**File: `garminsync/web/app.py`**
|
|
||||||
|
|
||||||
Add activities route:
|
|
||||||
```python
|
|
||||||
@app.get("/activities")
|
|
||||||
async def activities_page(request: Request):
|
|
||||||
"""Activities page route"""
|
|
||||||
if not templates:
|
|
||||||
return JSONResponse({"message": "Activities endpoint"})
|
|
||||||
|
|
||||||
return templates.TemplateResponse("activities.html", {
|
|
||||||
"request": request
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### Phase 7: Performance Optimization
|
|
||||||
|
|
||||||
#### 7.1 Lazy Loading and Pagination
|
|
||||||
- Implement virtual scrolling for large activity datasets
|
|
||||||
- Add progressive loading indicators
|
|
||||||
- Cache frequently accessed data
|
|
||||||
|
|
||||||
#### 7.2 Real-time Updates
|
|
||||||
- WebSocket integration for live sync status
|
|
||||||
- Progressive enhancement for users without JavaScript
|
|
||||||
- Offline support with service workers
|
|
||||||
|
|
||||||
## Testing Strategy
|
|
||||||
|
|
||||||
### 7.1 Manual Testing Checklist
|
|
||||||
- [ ] Home page layout matches mockup exactly
|
|
||||||
- [ ] Activities table displays with proper alternating colors
|
|
||||||
- [ ] Navigation works between pages
|
|
||||||
- [ ] Sync button functions correctly
|
|
||||||
- [ ] Statistics update in real-time
|
|
||||||
- [ ] Responsive design works on mobile
|
|
||||||
- [ ] All existing API endpoints still function
|
|
||||||
|
|
||||||
### 7.2 Browser Compatibility
|
|
||||||
- Test in Chrome, Firefox, Safari, Edge
|
|
||||||
- Ensure graceful degradation for older browsers
|
|
||||||
- Test JavaScript disabled scenarios
|
|
||||||
|
|
||||||
## Deployment Strategy
|
|
||||||
|
|
||||||
### 8.1 Staging Deployment
|
|
||||||
1. Deploy to test environment
|
|
||||||
2. Run automated tests
|
|
||||||
3. User acceptance testing
|
|
||||||
4. Performance benchmarking
|
|
||||||
|
|
||||||
### 8.2 Production Rollout
|
|
||||||
1. Feature flags for gradual rollout
|
|
||||||
2. Monitor error rates and performance
|
|
||||||
3. Rollback plan in case of issues
|
|
||||||
|
|
||||||
## Success Criteria
|
|
||||||
|
|
||||||
- [ ] UI matches provided mockups exactly
|
|
||||||
- [ ] All existing functionality preserved
|
|
||||||
- [ ] Page load times under 2 seconds
|
|
||||||
- [ ] Mobile responsive design works perfectly
|
|
||||||
- [ ] Real-time updates function correctly
|
|
||||||
- [ ] No breaking changes to API
|
|
||||||
- [ ] Comprehensive test coverage
|
|
||||||
|
|
||||||
## Timeline Estimate
|
|
||||||
|
|
||||||
- **Phase 1-2 (Backend/Architecture)**: 2-3 days
|
|
||||||
- **Phase 3 (Home Page)**: 2-3 days
|
|
||||||
- **Phase 4 (Activities Page)**: 2-3 days
|
|
||||||
- **Phase 5 (Styling/Polish)**: 1-2 days
|
|
||||||
- **Phase 6-7 (Integration/Testing)**: 1-2 days
|
|
||||||
|
|
||||||
**Total Estimated Time**: 8-13 days
|
|
||||||
|
|
||||||
This plan provides a comprehensive roadmap for transforming the existing GarminSync interface into the modern, clean design shown in the mockups while preserving all existing functionality.
|
|
||||||
Reference in New Issue
Block a user