Files
FitTrack2/FitnessSync/backend/templates/fitbit_health.html
2026-01-09 09:59:36 -08:00

329 lines
13 KiB
HTML

{% extends "base.html" %}
{% block content %}
<div class="row mb-4">
<div class="col-md-8">
<h1>Fitbit Health</h1>
<p class="text-muted">Track your weight and body composition from Fitbit.</p>
</div>
<div class="col-md-4 text-end">
<div class="btn-group" role="group">
<button id="syncBtn30" class="btn btn-primary" onclick="triggerSync(30)">
<i class="bi bi-arrow-repeat"></i> Sync Recent (30d)
</button>
<button id="syncBtnAll" class="btn btn-outline-primary" onclick="triggerSync(3650)">
<i class="bi bi-collection"></i> Sync All
</button>
<button id="compareBtn" class="btn btn-outline-info" onclick="compareWeights()">
<i class="bi bi-bar-chart-steps"></i> Compare vs Garmin
</button>
</div>
</div>
</div>
<!-- Filters -->
<div class="card mb-4">
<div class="card-body">
<form id="filterForm" class="row g-3 align-items-end">
<div class="col-md-3">
<label for="startDate" class="form-label">Start Date</label>
<input type="date" class="form-control" id="startDate" required>
</div>
<div class="col-md-3">
<label for="endDate" class="form-label">End Date</label>
<input type="date" class="form-control" id="endDate" required>
</div>
<div class="col-md-4">
<label class="form-label">Quick Filters</label>
<div class="btn-group w-100" role="group">
<button type="button" class="btn btn-outline-secondary" onclick="setRange(30)">30d</button>
<button type="button" class="btn btn-outline-secondary" onclick="setRange(365)">1y</button>
<button type="button" class="btn btn-outline-secondary" onclick="setRange(3650)">All</button>
</div>
</div>
</form>
</div>
</div>
<!-- Detailed Data Table -->
<div class="card">
<div class="card-header">
Weight Logs
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover" id="metricsTable">
<thead>
<tr>
<th>Date</th>
<th>Weight (kg)</th>
<th>BMI</th>
<th>Source</th>
</tr>
</thead>
<tbody>
<!-- Populated by JS -->
</tbody>
</table>
</div>
</div>
</div>
<!-- Comparison Modal -->
<div class="modal fade" id="compareModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Fitbit vs Garmin Weight Sync</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div id="compareLoading" class="text-center">
<div class="spinner-border text-primary" role="status"></div>
<p>Comparing records...</p>
</div>
<div id="compareResults" class="d-none">
<h5>Summary</h5>
<ul class="list-group mb-3">
<li class="list-group-item d-flex justify-content-between align-items-center">
Fitbit Records
<span class="badge bg-primary rounded-pill" id="compFitbit">0</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
Garmin Records
<span class="badge bg-success rounded-pill" id="compGarmin">0</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
Missing in Garmin
<span class="badge bg-danger rounded-pill" id="compMissing">0</span>
</li>
</ul>
<div class="d-grid gap-2 mb-3">
<button id="uploadMissingBtn" class="btn btn-warning" onclick="triggerGarminUpload()">
<i class="bi bi-cloud-upload"></i> Upload Missing to Garmin (Batch 50)
</button>
</div>
<h5>Missing Dates</h5>
<p class="small text-muted">These dates exist in Fitbit but not Garmin.</p>
<div id="missingDatesList"
style="max-height: 200px; overflow-y: auto; border: 1px solid #dee2e6; padding: 10px;">
<!-- Populated by JS -->
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<script>
// Initialize dates (last 30 days)
const today = new Date();
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(today.getDate() - 30);
document.getElementById('endDate').valueAsDate = today;
document.getElementById('startDate').valueAsDate = thirtyDaysAgo;
document.getElementById('filterForm').addEventListener('submit', function (e) {
e.preventDefault();
loadData();
});
function setRange(days) {
const e = new Date();
const s = new Date();
s.setDate(e.getDate() - days);
document.getElementById('endDate').valueAsDate = e;
document.getElementById('startDate').valueAsDate = s;
loadData();
}
async function triggerSync(daysBack) {
const confirmMsg = daysBack > 100 ? "Sync all historical data? This may take a while." : "Sync Fitbit weight for last 30 days?";
if (!confirm(confirmMsg)) return;
const scope = daysBack > 100 ? 'all' : '30d';
try {
const res = await fetch('/api/sync/fitbit/weight', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ scope: scope })
});
if (!res.ok) {
const errData = await res.json();
throw new Error(errData.detail || 'Sync failed');
}
const data = await res.json();
showToast('Sync started/completed: ' + (data.message || 'Success'));
// Since it's currently sync (based on backend), reload data immediately
loadData();
} catch (e) {
console.error(e);
showToast('Error executing sync: ' + e.message, 'danger');
}
}
async function pollJob(jobId) {
const check = async () => {
try {
const res = await fetch(`/api/jobs/${jobId}`);
if (res.status === 404) {
loadData();
showToast('Sync finished (or job cleared).');
return;
}
const data = await res.json();
if (data.status === 'completed' || data.status === 'failed') {
loadData();
showToast(`Sync ${data.status}: ${data.message || ''}`, data.status === 'failed' ? 'danger' : 'success');
} else {
setTimeout(check, 2000);
}
} catch (e) { console.error(e); }
};
check();
}
async function compareWeights() {
// Show modal
const modal = new bootstrap.Modal(document.getElementById('compareModal'));
modal.show();
document.getElementById('compareLoading').classList.remove('d-none');
document.getElementById('compareResults').classList.add('d-none');
try {
const res = await fetch('/api/sync/compare-weight', { method: 'POST' });
const data = await res.json();
document.getElementById('compFitbit').textContent = data.fitbit_total;
document.getElementById('compGarmin').textContent = data.garmin_total;
document.getElementById('compMissing').textContent = data.missing_in_garmin;
const listDiv = document.getElementById('missingDatesList');
listDiv.innerHTML = '';
if (data.missing_dates && data.missing_dates.length > 0) {
data.missing_dates.forEach(d => {
const div = document.createElement('div');
div.textContent = d;
div.className = 'border-bottom py-1';
listDiv.appendChild(div);
});
} else {
listDiv.innerHTML = '<div class="text-success text-center my-3">All clear! All Fitbit dates are present in Garmin.</div>';
}
document.getElementById('compareLoading').classList.add('d-none');
document.getElementById('compareResults').classList.remove('d-none');
} catch (e) {
console.error(e);
document.getElementById('compareLoading').innerHTML = '<p class="text-danger">Error fetching comparison.</p>';
}
}
async function triggerGarminUpload() {
if (!confirm("This will upload up to 50 unsynced weight records to Garmin Connect. Continue?")) return;
const btn = document.getElementById('uploadMissingBtn');
const originalText = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Uploading...';
try {
const res = await fetch('/api/sync/garmin/upload_weight', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ limit: 50 })
});
const data = await res.json();
showToast('Upload job started: ' + data.job_id);
pollJob(data.job_id);
} catch (e) {
console.error(e);
showToast('Error starting upload: ' + e.message, 'danger');
} finally {
btn.disabled = false;
btn.innerHTML = originalText;
}
}
async function loadData() {
const start = document.getElementById('startDate').value;
const end = document.getElementById('endDate').value;
const tbody = document.querySelector('#metricsTable tbody');
tbody.innerHTML = '<tr><td colspan="4" class="text-center">Loading...</td></tr>';
try {
const url = `/api/metrics/query?metric_type=weight&start_date=${start}&end_date=${end}&source=fitbit&limit=5000`;
const response = await fetch(url);
const data = await response.json();
tbody.innerHTML = '';
if (data.length === 0) {
tbody.innerHTML = '<tr><td colspan="4" class="text-center">No data found</td></tr>';
return;
}
// Sort by date desc
data.sort((a, b) => new Date(b.date) - new Date(a.date));
data.forEach(item => {
// Parse detailed data if available for BMI
let bmi = '-';
try {
if (item.detailed_data) {
// It might be a string or object depending on serialization
const details = (typeof item.detailed_data === 'string')
? JSON.parse(item.detailed_data)
: item.detailed_data;
if (details && details.bmi) bmi = details.bmi;
}
} catch (e) { }
const tr = document.createElement('tr');
tr.innerHTML = `
<td>${item.date}</td>
<td>${Number(item.metric_value).toFixed(1)}</td>
<td>${bmi}</td>
<td><span class="badge bg-success">Fitbit</span></td>
`;
tbody.appendChild(tr);
});
} catch (error) {
console.error('Error loading metrics:', error);
tbody.innerHTML = '<tr><td colspan="4" class="text-danger">Error loading data</td></tr>';
}
}
// Initial load
loadData();
function showToast(msg, type = 'success') {
const el = document.getElementById('appToast');
if (el) {
const toast = new bootstrap.Toast(el);
document.querySelector('#appToast .toast-body').textContent = msg;
toast.show();
} else {
alert(msg);
}
}
</script>
{% endblock %}