added activity view
This commit is contained in:
221
FitnessSync/backend/templates/base.html
Normal file
221
FitnessSync/backend/templates/base.html
Normal file
@@ -0,0 +1,221 @@
|
||||
<!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>
|
||||
Reference in New Issue
Block a user