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
- Deploy to test environment
- Run automated tests
- User acceptance testing
- Performance benchmarking
8.2 Production Rollout
- Feature flags for gradual rollout
- Monitor error rates and performance
- 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.