Files
FitTrack2/FitnessSync/backend/templates/base.html
2026-01-09 12:10:58 -08:00

224 lines
9.5 KiB
HTML

<!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">
<!-- Bootstrap Icons -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css">
<style>
/* Shared Styles */
.card {
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
border: none;
}
.nav-tabs .nav-link {
color: #495057;
}
.nav-tabs .nav-link.active {
font-weight: bold;
}
</style>
{% block head %}{% endblock %}
</head>
<body>
<div class="container mt-5">
<div class="row mb-4 align-items-center">
<div class="col-auto">
<a href="/" class="text-decoration-none text-dark">
<h1>Fitbit-Garmin Sync Dashboard</h1>
</a>
</div>
<div class="col">
<!-- Global Sync Status Widget -->
<div id="global-sync-status" class="card shadow-sm"
style="display: none; border-left: 5px solid #0d6efd;">
<div class="card-body py-2 px-3">
<div class="d-flex align-items-center justify-content-between">
<div class="flex-grow-1 me-3">
<div class="d-flex justify-content-between align-items-center mb-1">
<strong class="small" id="global-job-op">Operation</strong>
<span class="badge bg-secondary ms-2" id="global-queue-badge"
style="display: none;">Queue: 0</span>
</div>
<div class="progress" style="height: 10px;">
<div id="global-job-bar"
class="progress-bar progress-bar-striped progress-bar-animated"
role="progressbar" style="width: 0%"></div>
</div>
<small id="global-job-msg" class="text-muted d-block text-truncate"
style="max-width: 400px; font-size: 0.75rem;">Initializing...</small>
</div>
<button class="btn btn-outline-danger btn-sm" id="global-stop-btn">
<i class="bi bi-stop-circle"></i> Stop
</button>
</div>
</div>
</div>
</div>
</div>
<ul class="nav nav-tabs mb-4">
<li class="nav-item">
<a class="nav-link {% if request.path == '/' %}active{% endif %}" href="/">Dashboard</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.path == '/activities' %}active{% endif %}"
href="/activities">Activities</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.path == '/segments' %}active{% endif %}" href="/segments">Segments</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.path == '/garmin-health' %}active{% endif %}"
href="/garmin-health">Garmin Health</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.path == '/fitbit-health' %}active{% endif %}"
href="/fitbit-health">Fitbit Health</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.path == '/bike-setups' %}active{% endif %}" href="/bike-setups">Bike
Setups</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.path == '/setup' %}active{% endif %}" href="/setup">Setup</a>
</li>
</ul>
<!-- Toast container -->
<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>
{% block content %}{% endblock %}
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
// Shared Toast Helper
function showToast(msg, type = 'info') {
const el = document.getElementById('appToast');
if (el) {
const toast = new bootstrap.Toast(el);
const header = el.querySelector('.toast-header');
const body = el.querySelector('.toast-body');
body.textContent = msg;
header.classList.remove('bg-success', 'bg-danger', 'bg-warning', 'bg-info', 'text-white');
if (type === 'success') header.classList.add('bg-success', 'text-white');
if (type === 'danger' || type === 'error') header.classList.add('bg-danger', 'text-white');
if (type === 'warning') header.classList.add('bg-warning');
toast.show();
} else {
console.log("Toast:", msg);
}
}
// Global Sync Status Poller
let globalPollInterval = null;
let globalCurrentJobId = null;
function startGlobalPolling() {
if (globalPollInterval) clearInterval(globalPollInterval);
checkGlobalJobStatus(); // Immediate check
globalPollInterval = setInterval(checkGlobalJobStatus, 2000);
}
async function checkGlobalJobStatus() {
try {
const response = await fetch('/api/jobs/active');
if (response.ok) {
const jobs = await response.json();
const widget = document.getElementById('global-sync-status');
const opEl = document.getElementById('global-job-op');
const msgEl = document.getElementById('global-job-msg');
const barEl = document.getElementById('global-job-bar');
const stopBtn = document.getElementById('global-stop-btn');
const queueBadge = document.getElementById('global-queue-badge');
if (jobs.length > 0) {
const job = jobs[0];
globalCurrentJobId = job.id;
// Show Widget
widget.style.display = 'block';
// Update UI
opEl.textContent = job.operation;
msgEl.textContent = job.message;
barEl.style.width = job.progress + '%';
if (jobs.length > 1) {
queueBadge.style.display = 'inline-block';
queueBadge.textContent = `Queue: ${jobs.length - 1}`;
} else {
queueBadge.style.display = 'none';
}
// Stop Button State
if (job.cancel_requested) {
stopBtn.disabled = true;
stopBtn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Stopping...';
} else {
stopBtn.disabled = false;
stopBtn.innerHTML = '<i class="bi bi-stop-circle"></i> Stop';
stopBtn.onclick = () => stopGlobalJob(job.id);
}
// Dispatch event for other pages
document.dispatchEvent(new CustomEvent('sync-job-active', { detail: { job: job } }));
} else {
// No jobs
if (widget.style.display !== 'none') {
// Job just finished
showToast('Background Job Completed', 'success');
document.dispatchEvent(new CustomEvent('sync-job-finished'));
}
widget.style.display = 'none';
globalCurrentJobId = null;
}
}
} catch (e) {
console.error("Global polling error", e);
}
}
async function stopGlobalJob(jobId) {
if (!jobId) return;
try {
const response = await fetch(`/api/jobs/${jobId}/stop`, { method: 'POST' });
if (response.ok) {
showToast('Stopping Job...', 'warning');
// Force immediate update to disable button
checkGlobalJobStatus();
}
} catch (e) {
showToast('Failed to stop job', 'danger');
}
}
document.addEventListener('DOMContentLoaded', startGlobalPolling);
</script>
{% block scripts %}{% endblock %}
</body>
</html>