Files
GarminSync/ui_plan.md
2025-08-19 07:09:23 -07:00

20 KiB

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:

@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:

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 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:

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

{% 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

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

{% 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

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

/* 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

/* 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

<!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:

@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.