feat(dashboard): 3-col layout with Overview stats, speed chart fixes

- base.html: fix notification badge positioning (inline style, no overflow
  clip); remove py-4 from main; update footer text
- index.html: split bottom row into 3 cols (GPS / Overview / Speed);
  Overview card with 6 live stats from /api/stats; speed chart auto-expands
  on desktop; shown.bs.collapse triggers speedChart.resize()

Co-authored-by: Copilot <[email protected]>
This commit is contained in:
Mathieu Moran
2026-08-10 00:13:23 -04:00
co-authored by Copilot
parent ea03ef7f6c
commit 5d9b2f6556
2 changed files with 72 additions and 12 deletions
+3 -3
View File
@@ -68,7 +68,7 @@
<button class="btn btn-link position-relative p-2" id="alert-toggle" type="button" <button class="btn btn-link position-relative p-2" id="alert-toggle" type="button"
data-bs-toggle="dropdown" aria-expanded="false" aria-label="System alerts"> data-bs-toggle="dropdown" aria-expanded="false" aria-label="System alerts">
<i class="fas fa-bell fa-fw text-secondary"></i> <i class="fas fa-bell fa-fw text-secondary"></i>
<span id="alert-count" class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger" hidden></span> <span id="alert-count" class="position-absolute badge rounded-pill bg-danger" style="top:2px;right:2px;font-size:.6rem;min-width:1.1em;padding:.2em .35em;" hidden></span>
</button> </button>
<div class="dropdown-menu dropdown-menu-end shadow" id="alert-menu" aria-labelledby="alert-toggle"> <div class="dropdown-menu dropdown-menu-end shadow" id="alert-menu" aria-labelledby="alert-toggle">
<h6 class="dropdown-header">System alerts</h6> <h6 class="dropdown-header">System alerts</h6>
@@ -84,12 +84,12 @@
</div> </div>
</nav> </nav>
<main class="container-fluid py-4 px-3 px-md-4 main-content"> <main class="container-fluid px-3 px-md-4 main-content">
{% block content %}{% endblock %} {% block content %}{% endblock %}
</main> </main>
<footer class="bg-white border-top text-center py-3 small text-muted d-none d-md-block"> <footer class="bg-white border-top text-center py-3 small text-muted d-none d-md-block">
&copy; Yaris OBD {{ car.model }} &copy; vroom vroom yaris de course 2023
</footer> </footer>
</div> </div>
+69 -9
View File
@@ -91,9 +91,9 @@
</div> </div>
</div> </div>
{# ── GPS map (with inline coords footer) ─────────────────────────────── #} {# ── GPS map + Stats + Speed history ──────────────────────────────────── #}
<div class="row g-3 mb-3"> <div class="row g-3 mb-3">
<div class="col-lg-5 col-xl-4"> <div class="col-lg-4 col-xl-3">
<div class="card shadow-sm h-100"> <div class="card shadow-sm h-100">
<div class="card-header py-2"> <div class="card-header py-2">
<h6 class="text-primary fw-bold m-0">Live GPS</h6> <h6 class="text-primary fw-bold m-0">Live GPS</h6>
@@ -113,16 +113,50 @@
</div> </div>
</div> </div>
</div> </div>
<div class="col-lg-7 col-xl-8"> <div class="col-lg-3 col-xl-2">
<div class="card shadow-sm h-100"> <div class="card shadow-sm h-100">
<div class="card-header py-2 d-flex align-items-center gap-3"> <div class="card-header py-2">
<h6 class="text-primary fw-bold m-0">Overview</h6>
</div>
<div class="card-body py-3 px-3" id="stats-body">
<div class="row g-3">
<div class="col-6 col-lg-12">
<div class="text-xs text-muted fw-bold text-uppercase mb-1">Trips</div>
<div class="fw-bold fs-5 text-dark" id="stat-trips">—</div>
</div>
<div class="col-6 col-lg-12">
<div class="text-xs text-muted fw-bold text-uppercase mb-1">Total distance</div>
<div class="fw-bold fs-5 text-dark" id="stat-km">—</div>
</div>
<div class="col-6 col-lg-12">
<div class="text-xs text-muted fw-bold text-uppercase mb-1">Drive time</div>
<div class="fw-bold fs-5 text-dark" id="stat-time">—</div>
</div>
<div class="col-6 col-lg-12">
<div class="text-xs text-muted fw-bold text-uppercase mb-1">Top speed ever</div>
<div class="fw-bold fs-5 text-dark" id="stat-topspeed">—</div>
</div>
<div class="col-6 col-lg-12">
<div class="text-xs text-muted fw-bold text-uppercase mb-1">Videos on Pi</div>
<div class="fw-bold fs-5 text-dark" id="stat-videos">—</div>
</div>
<div class="col-6 col-lg-12">
<div class="text-xs text-muted fw-bold text-uppercase mb-1">Active DTC</div>
<div class="fw-bold fs-5" id="stat-dtc">—</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-lg-5 col-xl-7">
<div class="card shadow-sm h-100">
<div class="card-header py-2 d-flex align-items-center gap-2">
<h6 class="text-primary fw-bold m-0 me-auto">Speed history</h6> <h6 class="text-primary fw-bold m-0 me-auto">Speed history</h6>
{# Live speed badge — visible even when graph is collapsed #}
<span class="text-xs text-muted fw-bold text-uppercase">Now</span> <span class="text-xs text-muted fw-bold text-uppercase">Now</span>
<span id="speed-now-badge" class="fw-bold text-dark" style="min-width:3.5rem;text-align:right;">—</span> <span id="speed-now-badge" class="fw-bold text-dark" style="min-width:3rem;text-align:right;">—</span>
<span class="text-xs text-muted fw-bold text-uppercase ms-2">Max</span> <span class="text-xs text-muted fw-bold text-uppercase ms-2 speed-max-group">Max</span>
<span id="speed-max-badge" class="fw-bold text-danger" style="min-width:3.5rem;text-align:right;">—</span> <span id="speed-max-badge" class="fw-bold text-danger speed-max-group" style="min-width:3rem;text-align:right;">—</span>
<button class="btn btn-link btn-sm p-0 text-muted ms-2" id="speed-graph-toggle" type="button" <button class="btn btn-link btn-sm p-0 text-muted ms-1" id="speed-graph-toggle" type="button"
aria-expanded="false" aria-controls="speed-graph-body"> aria-expanded="false" aria-controls="speed-graph-body">
<i class="fas fa-chevron-down"></i> <i class="fas fa-chevron-down"></i>
</button> </button>
@@ -158,11 +192,37 @@
btn.setAttribute('aria-expanded', 'true'); btn.setAttribute('aria-expanded', 'true');
btn.querySelector('i').className = 'fas fa-chevron-up'; btn.querySelector('i').className = 'fas fa-chevron-up';
}); });
body.addEventListener('shown.bs.collapse', function () {
if (window.speedChart) window.speedChart.resize();
});
body.addEventListener('hide.bs.collapse', function () { body.addEventListener('hide.bs.collapse', function () {
btn.setAttribute('aria-expanded', 'false'); btn.setAttribute('aria-expanded', 'false');
btn.querySelector('i').className = 'fas fa-chevron-down'; btn.querySelector('i').className = 'fas fa-chevron-down';
}); });
btn.addEventListener('click', function () { bsCollapse.toggle(); }); btn.addEventListener('click', function () { bsCollapse.toggle(); });
// Auto-expand on desktop; stay collapsed on mobile
if (window.innerWidth >= 768) {
bsCollapse.show();
}
})();
// Overview stats card
(function () {
function fmtDuration(s) {
const h = Math.floor(s / 3600);
const m = Math.floor((s % 3600) / 60);
return h > 0 ? h + 'h ' + m + 'm' : m + 'm';
}
fetch('/api/stats').then(r => r.json()).then(d => {
document.getElementById('stat-trips').textContent = d.trip_count;
document.getElementById('stat-km').textContent = d.total_km.toFixed(1) + ' km';
document.getElementById('stat-time').textContent = d.total_duration_s > 0 ? fmtDuration(d.total_duration_s) : '—';
document.getElementById('stat-topspeed').textContent = d.all_time_max_speed > 0 ? d.all_time_max_speed + ' km/h' : '—';
document.getElementById('stat-videos').textContent = d.video_count;
const dtcEl = document.getElementById('stat-dtc');
dtcEl.textContent = d.active_dtc;
dtcEl.className = 'fw-bold fs-5 ' + (d.active_dtc > 0 ? 'text-danger' : 'text-success');
}).catch(() => {});
})(); })();
</script> </script>
{% endblock %} {% endblock %}