feat: Update spec, fix bugs, improve UI/UX, and clean up code
This commit is contained in:
@@ -10,18 +10,20 @@
|
||||
<div class="container mt-5">
|
||||
<h1>Fitbit-Garmin Sync Dashboard</h1>
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Weight Records</h5>
|
||||
<p class="card-text">Total: <span id="total-weights">0</span></p>
|
||||
<p class="card-text">Synced: <span id="synced-weights">0</span></p>
|
||||
<p class="card-text">Unsynced: <span id="unsynced-weights">0</span></p>
|
||||
</div>
|
||||
<!-- Toast container for notifications -->
|
||||
<div class="toast-container position-fixed bottom-0 end-0 p-3">
|
||||
<div id="appToast" class="toast" role="alert" aria-live="assertive" aria-atomic="true">
|
||||
<div class="toast-header">
|
||||
<strong class="me-auto" id="toast-title">Notification</strong>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="toast-body" id="toast-body">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
</div>
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Activities</h5>
|
||||
@@ -30,13 +32,13 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Sync Status</h5>
|
||||
<h5 class="card-title">Sync Controls</h5>
|
||||
<div class="d-grid gap-2">
|
||||
<button class="btn btn-primary" type="button" id="sync-weight-btn">Sync Weight</button>
|
||||
<button class="btn btn-secondary" type="button" id="sync-activities-btn">Sync Activities</button>
|
||||
<button class="btn btn-primary" type="button" id="sync-activities-btn">Sync Activities</button>
|
||||
<button class="btn btn-info" type="button" id="sync-metrics-btn">Sync Health Metrics</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -53,13 +55,15 @@
|
||||
<th>Operation</th>
|
||||
<th>Status</th>
|
||||
<th>Start Time</th>
|
||||
<th>Records Processed</th>
|
||||
<th>Records Failed</th>
|
||||
<th>End Time</th>
|
||||
<th>Processed</th>
|
||||
<th>Failed</th>
|
||||
<th>Message</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td colspan="5">Loading logs...</td>
|
||||
<td colspan="7">Loading logs...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -69,71 +73,11 @@
|
||||
|
||||
<div class="row mt-5">
|
||||
<div class="col-md-12">
|
||||
<h3>Health Metrics</h3>
|
||||
<h3>Actions</h5>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="d-grid gap-2 d-md-flex justify-content-md-start">
|
||||
<button class="btn btn-info me-md-2" type="button" id="sync-metrics-btn">Sync Health Metrics</button>
|
||||
<button class="btn btn-outline-info me-md-2" type="button" id="view-metrics-btn">View Health Data Summary</button>
|
||||
<button class="btn btn-outline-info" type="button" id="query-metrics-btn">Query Metrics</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-5">
|
||||
<div class="col-md-12">
|
||||
<h3>Activity Files</h3>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="d-grid gap-2 d-md-flex justify-content-md-start">
|
||||
<button class="btn btn-outline-secondary me-md-2" type="button" id="list-activities-btn">List Stored Activities</button>
|
||||
<button class="btn btn-outline-secondary" type="button" id="download-activities-btn">Download Activity File</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-5">
|
||||
<div class="col-md-12">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="d-grid gap-2 d-md-flex justify-content-md-start">
|
||||
<a href="/setup" class="btn btn-primary me-md-2">Setup & Configuration</a>
|
||||
<a href="/docs" class="btn btn-outline-secondary" target="_blank">API Documentation</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-5">
|
||||
<div class="col-md-12">
|
||||
<h3>Health Metrics</h3>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="d-grid gap-2 d-md-flex justify-content-md-start">
|
||||
<button class="btn btn-info me-md-2" type="button" id="sync-metrics-btn">Sync Health Metrics</button>
|
||||
<button class="btn btn-outline-info me-md-2" type="button" id="view-metrics-btn">View Health Data Summary</button>
|
||||
<button class="btn btn-outline-info" type="button" id="query-metrics-btn">Query Metrics</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-5">
|
||||
<div class="col-md-12">
|
||||
<h3>Activity Files</h3>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="d-grid gap-2 d-md-flex justify-content-md-start">
|
||||
<button class="btn btn-outline-secondary me-md-2" type="button" id="list-activities-btn">List Stored Activities</button>
|
||||
<button class="btn btn-outline-secondary" type="button" id="download-activities-btn">Download Activity File</button>
|
||||
</div>
|
||||
<a href="/setup" class="btn btn-primary me-md-2">Setup & Configuration</a>
|
||||
<a href="/docs" class="btn btn-outline-secondary" target="_blank">API Documentation</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -142,167 +86,124 @@
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script>
|
||||
// Load dashboard data when page loads
|
||||
let toastInstance = null;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const toastEl = document.getElementById('appToast');
|
||||
toastInstance = new bootstrap.Toast(toastEl);
|
||||
|
||||
loadDashboardData();
|
||||
|
||||
// Set up sync buttons
|
||||
document.getElementById('sync-weight-btn').addEventListener('click', syncWeight);
|
||||
document.getElementById('sync-activities-btn').addEventListener('click', syncActivities);
|
||||
|
||||
// Set up metrics buttons
|
||||
document.getElementById('sync-metrics-btn').addEventListener('click', syncHealthMetrics);
|
||||
document.getElementById('view-metrics-btn').addEventListener('click', viewHealthSummary);
|
||||
document.getElementById('query-metrics-btn').addEventListener('click', queryMetrics);
|
||||
|
||||
// Set up activity file buttons
|
||||
document.getElementById('list-activities-btn').addEventListener('click', listActivities);
|
||||
document.getElementById('download-activities-btn').addEventListener('click', downloadActivityFile);
|
||||
});
|
||||
|
||||
function showToast(title, body, level = 'info') {
|
||||
const toastTitle = document.getElementById('toast-title');
|
||||
const toastBody = document.getElementById('toast-body');
|
||||
const toastHeader = document.querySelector('.toast-header');
|
||||
|
||||
toastTitle.textContent = title;
|
||||
toastBody.textContent = body;
|
||||
|
||||
// Reset header color
|
||||
toastHeader.classList.remove('bg-success', 'bg-danger', 'bg-warning', 'bg-info', 'text-white');
|
||||
|
||||
if (level === 'success') {
|
||||
toastHeader.classList.add('bg-success', 'text-white');
|
||||
} else if (level === 'error') {
|
||||
toastHeader.classList.add('bg-danger', 'text-white');
|
||||
} else if (level === 'warning') {
|
||||
toastHeader.classList.add('bg-warning');
|
||||
} else {
|
||||
toastHeader.classList.add('bg-info', 'text-white');
|
||||
}
|
||||
|
||||
toastInstance.show();
|
||||
}
|
||||
|
||||
async function loadDashboardData() {
|
||||
try {
|
||||
const response = await fetch('/api/status');
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
|
||||
document.getElementById('total-weights').textContent = data.total_weight_records;
|
||||
document.getElementById('synced-weights').textContent = data.synced_weight_records;
|
||||
document.getElementById('unsynced-weights').textContent = data.unsynced_weight_records;
|
||||
document.getElementById('total-activities').textContent = data.total_activities;
|
||||
document.getElementById('downloaded-activities').textContent = data.downloaded_activities;
|
||||
|
||||
// Update logs table
|
||||
const logsBody = document.querySelector('#sync-logs-table tbody');
|
||||
logsBody.innerHTML = '';
|
||||
|
||||
if (data.recent_logs.length === 0) {
|
||||
logsBody.innerHTML = '<tr><td colspan="7">No recent sync logs.</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
data.recent_logs.forEach(log => {
|
||||
const row = document.createElement('tr');
|
||||
row.innerHTML = `
|
||||
<td>${log.operation}</td>
|
||||
<td>${log.status}</td>
|
||||
<td>${log.start_time}</td>
|
||||
<td><span class="badge bg-${log.status === 'completed' ? 'success' : 'warning'}">${log.status}</span></td>
|
||||
<td>${new Date(log.start_time).toLocaleString()}</td>
|
||||
<td>${log.end_time ? new Date(log.end_time).toLocaleString() : 'N/A'}</td>
|
||||
<td>${log.records_processed}</td>
|
||||
<td>${log.records_failed}</td>
|
||||
<td>${log.message || ''}</td>
|
||||
`;
|
||||
logsBody.appendChild(row);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error loading dashboard data:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function syncWeight() {
|
||||
try {
|
||||
const response = await fetch('/api/sync/weight', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
alert(`Weight sync initiated: ${data.message}`);
|
||||
|
||||
// Refresh dashboard data
|
||||
loadDashboardData();
|
||||
} catch (error) {
|
||||
console.error('Error syncing weight:', error);
|
||||
alert('Error initiating weight sync: ' + error.message);
|
||||
showToast('Error', 'Could not load dashboard data.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function syncActivities() {
|
||||
showToast('Syncing...', 'Activity sync has been initiated.', 'info');
|
||||
try {
|
||||
const response = await fetch('/api/sync/activities', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ days_back: 30 })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.detail || `HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
alert(`Activity sync initiated: ${data.message}`);
|
||||
|
||||
// Refresh dashboard data
|
||||
loadDashboardData();
|
||||
showToast('Sync Complete', data.message, 'success');
|
||||
loadDashboardData(); // Refresh data after sync
|
||||
} catch (error) {
|
||||
console.error('Error syncing activities:', error);
|
||||
alert('Error initiating activity sync: ' + error.message);
|
||||
showToast('Sync Error', `Activity sync failed: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function syncHealthMetrics() {
|
||||
showToast('Syncing...', 'Health metrics sync has been initiated.', 'info');
|
||||
try {
|
||||
const response = await fetch('/api/sync/metrics', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.detail || `HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
alert(`Health metrics sync initiated: ${data.message}`);
|
||||
showToast('Sync Complete', data.message, 'success');
|
||||
loadDashboardData(); // Refresh data after sync
|
||||
} catch (error) {
|
||||
console.error('Error syncing health metrics:', error);
|
||||
alert('Error initiating health metrics sync: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function viewHealthSummary() {
|
||||
try {
|
||||
const response = await fetch('/api/health-data/summary');
|
||||
const data = await response.json();
|
||||
|
||||
alert(`Health Summary:
|
||||
Steps: ${data.total_steps || 0}
|
||||
Avg Heart Rate: ${data.avg_heart_rate || 0}
|
||||
Sleep Hours: ${data.total_sleep_hours || 0}
|
||||
Avg Calories: ${data.avg_calories || 0}`);
|
||||
} catch (error) {
|
||||
console.error('Error fetching health summary:', error);
|
||||
alert('Error fetching health summary: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function queryMetrics() {
|
||||
try {
|
||||
const response = await fetch('/api/metrics/query');
|
||||
const data = await response.json();
|
||||
|
||||
alert(`Found ${data.length} health metrics`);
|
||||
} catch (error) {
|
||||
console.error('Error querying metrics:', error);
|
||||
alert('Error querying metrics: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function listActivities() {
|
||||
try {
|
||||
const response = await fetch('/api/activities/list');
|
||||
const data = await response.json();
|
||||
|
||||
alert(`Found ${data.length} stored activities`);
|
||||
} catch (error) {
|
||||
console.error('Error listing activities:', error);
|
||||
alert('Error listing activities: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadActivityFile() {
|
||||
try {
|
||||
// For demo purposes, we'll use a placeholder ID
|
||||
// In a real implementation, this would prompt for activity ID or list available activities
|
||||
const activityId = prompt('Enter activity ID to download:', '12345');
|
||||
if (activityId) {
|
||||
// This would initiate a download of the stored activity file
|
||||
window.open(`/api/activities/download/${activityId}`, '_blank');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error downloading activity file:', error);
|
||||
alert('Error downloading activity file: ' + error.message);
|
||||
showToast('Sync Error', `Health metrics sync failed: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user