221 lines
9.3 KiB
HTML
221 lines
9.3 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 == '/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> |