working
This commit is contained in:
@@ -1,15 +1,29 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Fitbit-Garmin Sync Dashboard</title>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container mt-5">
|
||||
<h1>Fitbit-Garmin Sync Dashboard</h1>
|
||||
|
||||
|
||||
<ul class="nav nav-tabs mb-4">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" aria-current="page" href="/">Dashboard</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/activities">Activities</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/setup">Setup</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<!-- 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">
|
||||
@@ -22,13 +36,46 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Job Status Banner -->
|
||||
<div id="job-status-banner" class="alert alert-info mt-3" style="display: none;">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<strong id="job-operation">Operation</strong>
|
||||
<div class="progress mt-2" style="width: 300px;">
|
||||
<div id="job-progress-bar" class="progress-bar progress-bar-striped progress-bar-animated"
|
||||
role="progressbar" style="width: 0%"></div>
|
||||
</div>
|
||||
<small id="job-message" class="text-muted">Starting...</small>
|
||||
</div>
|
||||
<button class="btn btn-danger btn-sm" id="stop-job-btn">Stop</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Activities</h5>
|
||||
<p class="card-text">Total: <span id="total-activities">0</span></p>
|
||||
<p class="card-text">Downloaded: <span id="downloaded-activities">0</span></p>
|
||||
<h5 class="card-title">Last Sync Status</h5>
|
||||
<div class="table-responsive" style="max-height: 300px; overflow-y: auto;">
|
||||
<table class="table table-sm" id="metrics-status-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Type</th>
|
||||
<th>Source</th>
|
||||
<th>Found</th>
|
||||
<th>Synced</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td colspan="4">No sync data available.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="mt-2 text-muted small">
|
||||
<span id="db-stats"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -37,14 +84,29 @@
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Sync Controls</h5>
|
||||
<div class="d-grid gap-2">
|
||||
<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>
|
||||
<button class="btn btn-primary" type="button" id="sync-activities-btn">Sync Latest
|
||||
Activities (30d)</button>
|
||||
<button class="btn btn-outline-primary" type="button" id="sync-all-activities-btn">Sync All
|
||||
Historical Activities</button>
|
||||
<hr>
|
||||
<button class="btn btn-info text-white" type="button" id="sync-metrics-btn">Sync Latest
|
||||
Health Metrics (Garmin) (30d)</button>
|
||||
<button class="btn btn-outline-info" type="button" id="sync-all-metrics-btn">Sync All
|
||||
Historical Health Metrics (Garmin)</button>
|
||||
<hr>
|
||||
<h6 class="text-muted">Fitbit Sync</h6>
|
||||
<button class="btn btn-success" type="button" id="sync-fitbit-btn">Sync Latest Weight
|
||||
(Fitbit) (30d)</button>
|
||||
<button class="btn btn-outline-success" type="button" id="sync-all-fitbit-btn">Sync All
|
||||
Historical Weight (Fitbit)</button>
|
||||
<button class="btn btn-warning mt-2" type="button" id="compare-fitbit-btn">Compare Fitbit vs
|
||||
Garmin</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h3>Recent Sync Logs</h3>
|
||||
@@ -70,32 +132,43 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row mt-5">
|
||||
<div class="col-md-12">
|
||||
<h3>Actions</h5>
|
||||
<h3>Actions</h3>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<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>
|
||||
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script>
|
||||
let toastInstance = null;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
let isPolling = false;
|
||||
let currentJobId = null;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const toastEl = document.getElementById('appToast');
|
||||
toastInstance = new bootstrap.Toast(toastEl);
|
||||
|
||||
loadDashboardData();
|
||||
|
||||
document.getElementById('sync-activities-btn').addEventListener('click', syncActivities);
|
||||
document.getElementById('sync-metrics-btn').addEventListener('click', syncHealthMetrics);
|
||||
checkJobStatus(); // Check on load
|
||||
|
||||
document.getElementById('sync-activities-btn').addEventListener('click', () => syncActivities(30));
|
||||
document.getElementById('sync-all-activities-btn').addEventListener('click', () => syncActivities(3650));
|
||||
|
||||
document.getElementById('sync-metrics-btn').addEventListener('click', () => syncHealthMetrics(30));
|
||||
document.getElementById('sync-all-metrics-btn').addEventListener('click', () => syncHealthMetrics(3650));
|
||||
|
||||
document.getElementById('sync-fitbit-btn').addEventListener('click', () => syncFitbitWeight('30d'));
|
||||
document.getElementById('sync-all-fitbit-btn').addEventListener('click', () => syncFitbitWeight('all'));
|
||||
document.getElementById('compare-fitbit-btn').addEventListener('click', compareWeight);
|
||||
|
||||
document.getElementById('stop-job-btn').addEventListener('click', stopCurrentJob);
|
||||
});
|
||||
|
||||
function showToast(title, body, level = 'info') {
|
||||
@@ -105,7 +178,7 @@
|
||||
|
||||
toastTitle.textContent = title;
|
||||
toastBody.textContent = body;
|
||||
|
||||
|
||||
// Reset header color
|
||||
toastHeader.classList.remove('bg-success', 'bg-danger', 'bg-warning', 'bg-info', 'text-white');
|
||||
|
||||
@@ -121,7 +194,7 @@
|
||||
|
||||
toastInstance.show();
|
||||
}
|
||||
|
||||
|
||||
async function loadDashboardData() {
|
||||
try {
|
||||
const response = await fetch('/api/status');
|
||||
@@ -129,23 +202,41 @@
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
|
||||
document.getElementById('total-activities').textContent = data.total_activities;
|
||||
document.getElementById('downloaded-activities').textContent = data.downloaded_activities;
|
||||
|
||||
|
||||
document.getElementById('db-stats').innerHTML =
|
||||
`<strong>DB Total Activities:</strong> ${data.total_activities} | <strong>Downloaded:</strong> ${data.downloaded_activities}`;
|
||||
|
||||
const metricsBody = document.querySelector('#metrics-status-table tbody');
|
||||
metricsBody.innerHTML = '';
|
||||
|
||||
if (data.last_sync_stats && data.last_sync_stats.length > 0) {
|
||||
data.last_sync_stats.forEach(stat => {
|
||||
const row = document.createElement('tr');
|
||||
row.innerHTML = `
|
||||
<td>${stat.type}</td>
|
||||
<td>${stat.source}</td>
|
||||
<td>${stat.total}</td>
|
||||
<td class="${stat.synced > 0 ? 'text-success' : ''}">${stat.synced}</td>
|
||||
`;
|
||||
metricsBody.appendChild(row);
|
||||
});
|
||||
} else {
|
||||
metricsBody.innerHTML = '<tr><td colspan="4" class="text-center text-muted">No detailed sync stats available. Run a sync to populate.</td></tr>';
|
||||
}
|
||||
|
||||
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><span class="badge bg-${log.status === 'completed' ? 'success' : 'warning'}">${log.status}</span></td>
|
||||
<td><span class="badge bg-${log.status === 'completed' ? 'success' : (log.status === 'failed' || log.status === 'cancelled' ? 'danger' : '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>
|
||||
@@ -159,51 +250,185 @@
|
||||
showToast('Error', 'Could not load dashboard data.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function syncActivities() {
|
||||
showToast('Syncing...', 'Activity sync has been initiated.', 'info');
|
||||
|
||||
async function checkJobStatus() {
|
||||
try {
|
||||
const response = await fetch('/api/jobs/active');
|
||||
if (response.ok) {
|
||||
const jobs = await response.json();
|
||||
const banner = document.getElementById('job-status-banner');
|
||||
|
||||
if (jobs.length > 0) {
|
||||
const job = jobs[0]; // Just show first one for now
|
||||
currentJobId = job.id;
|
||||
isPolling = true;
|
||||
|
||||
banner.style.display = 'block';
|
||||
document.getElementById('job-operation').textContent = job.operation;
|
||||
document.getElementById('job-progress-bar').style.width = job.progress + '%';
|
||||
document.getElementById('job-message').textContent = job.message;
|
||||
|
||||
const stopBtn = document.getElementById('stop-job-btn');
|
||||
if (job.cancel_requested) {
|
||||
stopBtn.disabled = true;
|
||||
stopBtn.textContent = "Stopping...";
|
||||
} else {
|
||||
stopBtn.disabled = false;
|
||||
stopBtn.textContent = "Stop";
|
||||
}
|
||||
|
||||
// Disable sync buttons
|
||||
toggleSyncButtons(true);
|
||||
|
||||
setTimeout(checkJobStatus, 1000);
|
||||
} else {
|
||||
// Job finished
|
||||
if (isPolling) {
|
||||
showToast('Job Finished', 'Background job completed.', 'success');
|
||||
loadDashboardData();
|
||||
isPolling = false;
|
||||
currentJobId = null;
|
||||
banner.style.display = 'none';
|
||||
toggleSyncButtons(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Polling error", e);
|
||||
}
|
||||
}
|
||||
|
||||
function toggleSyncButtons(disabled) {
|
||||
const ids = [
|
||||
'sync-activities-btn', 'sync-all-activities-btn',
|
||||
'sync-metrics-btn', 'sync-all-metrics-btn',
|
||||
'sync-fitbit-btn', 'sync-all-fitbit-btn'
|
||||
];
|
||||
ids.forEach(id => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.disabled = disabled;
|
||||
});
|
||||
}
|
||||
|
||||
async function stopCurrentJob() {
|
||||
if (!currentJobId) return;
|
||||
try {
|
||||
const response = await fetch(`/api/jobs/${currentJobId}/stop`, { method: 'POST' });
|
||||
if (response.ok) {
|
||||
showToast('Stopping', 'Cancellation requested...', 'warning');
|
||||
document.getElementById('stop-job-btn').textContent = "Stopping...";
|
||||
document.getElementById('stop-job-btn').disabled = true;
|
||||
}
|
||||
} catch (e) {
|
||||
showToast('Error', 'Failed to stop job', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function syncActivities(daysBack = 30) {
|
||||
const typeLabel = daysBack > 1000 ? 'Historical' : 'Latest';
|
||||
showToast(`${typeLabel} Syncing...`, `Starting ${typeLabel} Activity sync...`, 'info');
|
||||
try {
|
||||
const response = await fetch('/api/sync/activities', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ days_back: 30 })
|
||||
body: JSON.stringify({ days_back: daysBack })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.detail || `HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
showToast('Sync Complete', data.message, 'success');
|
||||
loadDashboardData(); // Refresh data after sync
|
||||
|
||||
if (response.ok) {
|
||||
showToast('Sync Started', data.message, 'success');
|
||||
checkJobStatus();
|
||||
} else {
|
||||
throw new Error(data.detail || data.message || `HTTP error! status: ${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error syncing activities:', error);
|
||||
showToast('Sync Error', `Activity sync failed: ${error.message}`, 'error');
|
||||
showToast('Sync Error', `Activity sync failed start: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function syncHealthMetrics() {
|
||||
showToast('Syncing...', 'Health metrics sync has been initiated.', 'info');
|
||||
|
||||
async function syncHealthMetrics(daysBack = 30) {
|
||||
const typeLabel = daysBack > 1000 ? 'Historical' : 'Latest';
|
||||
showToast(`${typeLabel} Syncing...`, `Starting ${typeLabel} Health metrics sync...`, 'info');
|
||||
try {
|
||||
const response = await fetch('/api/sync/metrics', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ days_back: daysBack })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
showToast('Sync Started', data.message, 'success');
|
||||
checkJobStatus();
|
||||
} else {
|
||||
throw new Error(data.detail || data.message || `HTTP error! status: ${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error syncing health metrics:', error);
|
||||
showToast('Sync Error', `Health metrics sync failed start: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function syncFitbitWeight(scope) {
|
||||
const typeLabel = scope === 'all' ? 'All History' : 'Latest (30d)';
|
||||
showToast(`Fitbit Syncing...`, `Fitbit Weight sync initiated (${typeLabel}).`, 'info');
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/sync/fitbit/weight', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ scope: scope })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.detail || `HTTP error! status: ${response.status}`);
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.detail || errorData.message || `HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
|
||||
const data = await response.json();
|
||||
showToast('Sync Complete', data.message, 'success');
|
||||
loadDashboardData(); // Refresh data after sync
|
||||
showToast('Fitbit Sync Complete', data.message, 'success');
|
||||
loadDashboardData();
|
||||
} catch (error) {
|
||||
console.error('Error syncing health metrics:', error);
|
||||
showToast('Sync Error', `Health metrics sync failed: ${error.message}`, 'error');
|
||||
console.error('Error syncing Fitbit weight:', error);
|
||||
showToast('Fitbit Sync Error', `Sync failed: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function compareWeight() {
|
||||
showToast('Comparing...', 'Comparing Fitbit and Garmin weight records...', 'info');
|
||||
try {
|
||||
const response = await fetch('/api/sync/compare-weight', { method: 'POST' });
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.detail || `HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
|
||||
// Show a persistent alert or just a long toast?
|
||||
// A toast is fine for now, or maybe an alert.
|
||||
// Let's use a detailed toast.
|
||||
showToast('Comparison Results',
|
||||
`Fitbit Total: ${data.fitbit_total}\n` +
|
||||
`Garmin Total: ${data.garmin_total}\n` +
|
||||
`Missing in Garmin: ${data.missing_in_garmin}\n` +
|
||||
`${data.message}`,
|
||||
data.missing_in_garmin > 0 ? 'warning' : 'success'
|
||||
);
|
||||
|
||||
// Also log to console
|
||||
console.log("Comparison Data:", data);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error comparing weight:', error);
|
||||
showToast('Comparison Error', `Comparison failed: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
</html>
|
||||
Reference in New Issue
Block a user