Files
yaris-obd/server/templates/index.html
T
Mathieu MoranandCopilot 8f8c03a26a fix: server odometer stat shows 0 when no odometer data exists
COALESCE(MAX(odometer), 0) returned 0 when all tracking rows have NULL
odometer (expected before the car is connected — PID 0xA6 likely unsupported
on 2013 Yaris). Now returns NULL which the overview JS renders as '—'.

Co-authored-by: Copilot <[email protected]>
2026-08-10 05:58:22 -04:00

203 lines
8.8 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% block title %}Overview – Yaris OBD Remote{% endblock %}
{% block content %}
<h3 class="text-dark mb-3 fw-bold">Overview</h3>
<div class="row g-3 mb-4">
<div class="col-6 col-md-4 col-lg-2">
<div class="card shadow-sm h-100 text-center">
<div class="card-body py-3">
<div class="text-muted small mb-1"><i class="fas fa-route fa-fw"></i> Trips</div>
<div class="fw-bold fs-4" id="stat-trips">—</div>
</div>
</div>
</div>
<div class="col-6 col-md-4 col-lg-2">
<div class="card shadow-sm h-100 text-center">
<div class="card-body py-3">
<div class="text-muted small mb-1"><i class="fas fa-road fa-fw"></i> Distance</div>
<div class="fw-bold fs-4" id="stat-distance">—</div>
<div class="text-muted" style="font-size:.7rem;">km</div>
</div>
</div>
</div>
<div class="col-6 col-md-4 col-lg-2">
<div class="card shadow-sm h-100 text-center">
<div class="card-body py-3">
<div class="text-muted small mb-1"><i class="fas fa-clock fa-fw"></i> Drive time</div>
<div class="fw-bold fs-4" id="stat-drivetime">—</div>
</div>
</div>
</div>
<div class="col-6 col-md-4 col-lg-2">
<div class="card shadow-sm h-100 text-center">
<div class="card-body py-3">
<div class="text-muted small mb-1"><i class="fas fa-tachometer-alt fa-fw"></i> Top speed</div>
<div class="fw-bold fs-4" id="stat-topspeed">—</div>
<div class="text-muted" style="font-size:.7rem;">km/h</div>
</div>
</div>
</div>
<div class="col-6 col-md-4 col-lg-2">
<div class="card shadow-sm h-100 text-center">
<div class="card-body py-3">
<div class="text-muted small mb-1"><i class="fas fa-video fa-fw"></i> Videos</div>
<div class="fw-bold fs-4" id="stat-videos">—</div>
</div>
</div>
</div>
<div class="col-6 col-md-4 col-lg-2">
<div class="card shadow-sm h-100 text-center">
<div class="card-body py-3">
<div class="text-muted small mb-1"><i class="fas fa-gauge-high fa-fw"></i> Odometer</div>
<div class="fw-bold fs-4" id="stat-odometer">—</div>
<div class="text-muted" style="font-size:.7rem;">km</div>
</div>
</div>
</div>
</div>
<div class="row g-3">
<div class="col-12 col-lg-6">
<div class="card shadow-sm">
<div class="card-header py-2 d-flex justify-content-between align-items-center">
<h6 class="text-primary fw-bold m-0">Recent trips</h6>
<a href="{{ url_for('portal_history') }}" class="btn btn-outline-primary btn-sm">View all</a>
</div>
<div class="card-body p-0">
<div id="recent-trips-loading" class="text-center text-muted py-4">
<span class="spinner-border spinner-border-sm me-2" aria-hidden="true"></span> Loading…
</div>
<div class="table-responsive">
<table class="table table-sm table-hover mb-0 small" id="recent-trips-table" hidden>
<thead class="table-light">
<tr>
<th>Date</th>
<th>Distance</th>
<th>Time</th>
<th>Max km/h</th>
</tr>
</thead>
<tbody id="recent-trips-body"></tbody>
</table>
</div>
<div id="recent-trips-empty" class="text-muted text-center py-4 small" hidden>No trips recorded yet.</div>
</div>
</div>
</div>
<div class="col-12 col-lg-6">
<div class="card shadow-sm">
<div class="card-header py-2 d-flex justify-content-between align-items-center">
<h6 class="text-primary fw-bold m-0">Recent dashcam clips</h6>
<a href="{{ url_for('portal_dashcam') }}" class="btn btn-outline-primary btn-sm">View all</a>
</div>
<div class="card-body p-0">
<div id="recent-videos-loading" class="text-center text-muted py-4">
<span class="spinner-border spinner-border-sm me-2" aria-hidden="true"></span> Loading…
</div>
<div class="table-responsive">
<table class="table table-sm table-hover mb-0 small" id="recent-videos-table" hidden>
<thead class="table-light">
<tr>
<th>File</th>
<th>Size</th>
<th>Uploaded</th>
<th></th>
</tr>
</thead>
<tbody id="recent-videos-body"></tbody>
</table>
</div>
<div id="recent-videos-empty" class="text-muted text-center py-4 small" hidden>No dashcam files uploaded yet.</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
function fmtDuration(s) {
if (!s) return '—';
const h = Math.floor(s / 3600), m = Math.floor((s % 3600) / 60);
return h > 0 ? `${h}h ${m}m` : `${m}m`;
}
function fmtSize(b) {
if (b >= 1e9) return (b/1e9).toFixed(1) + ' GB';
if (b >= 1e6) return (b/1e6).toFixed(1) + ' MB';
return (b/1e3).toFixed(0) + ' KB';
}
function fmtDate(iso) {
return new Date(iso).toLocaleString('fr-CA', {month:'short', day:'numeric', hour:'2-digit', minute:'2-digit'});
}
async function loadStats() {
const res = await fetch('/api/v1/portal/stats');
if (!res.ok) return;
const d = await res.json();
document.getElementById('stat-trips').textContent = d.trip_count;
document.getElementById('stat-distance').textContent = d.total_distance_km.toFixed(0);
document.getElementById('stat-drivetime').textContent = fmtDuration(d.total_drive_time_s);
document.getElementById('stat-topspeed').textContent = d.top_speed_kmh.toFixed(0);
document.getElementById('stat-videos').textContent = d.video_count;
document.getElementById('stat-odometer').textContent = d.odometer_km != null ? d.odometer_km.toFixed(0) : '—';
}
async function loadRecentTrips() {
const loading = document.getElementById('recent-trips-loading');
const table = document.getElementById('recent-trips-table');
const empty = document.getElementById('recent-trips-empty');
try {
const res = await fetch('/api/v1/portal/trips?limit=8');
if (!res.ok) throw new Error();
const trips = await res.json();
loading.hidden = true;
if (!trips.length) { empty.hidden = false; return; }
const tbody = document.getElementById('recent-trips-body');
tbody.innerHTML = trips.map(t => `
<tr onclick="location.href='/history?trip=${t.id}'" style="cursor:pointer;">
<td>${fmtDate(t.start_datetime)}</td>
<td>${t.distance_km.toFixed(1)} km</td>
<td>${fmtDuration(t.duration_seconds)}</td>
<td>${t.max_speed_kmh ? t.max_speed_kmh.toFixed(0) : '—'}</td>
</tr>
`).join('');
table.hidden = false;
} catch { loading.hidden = true; empty.hidden = false; }
}
async function loadRecentVideos() {
const loading = document.getElementById('recent-videos-loading');
const table = document.getElementById('recent-videos-table');
const empty = document.getElementById('recent-videos-empty');
try {
const res = await fetch('/api/v1/portal/dashcam?limit=8');
if (!res.ok) throw new Error();
const files = await res.json();
loading.hidden = true;
if (!files.length) { empty.hidden = false; return; }
const tbody = document.getElementById('recent-videos-body');
tbody.innerHTML = files.map(f => {
const name = f.relative_path.split('/').pop();
return `
<tr>
<td class="text-truncate" style="max-width:160px;" title="${f.relative_path}">${name}</td>
<td class="text-nowrap">${fmtSize(f.size_bytes)}</td>
<td class="text-nowrap">${fmtDate(f.uploaded_at)}</td>
<td><a href="/api/v1/portal/dashcam/stream/${f.id}" class="btn btn-outline-secondary btn-sm py-0 px-1" title="Download">
<i class="fas fa-download fa-fw"></i></a></td>
</tr>
`;
}).join('');
table.hidden = false;
} catch { loading.hidden = true; empty.hidden = false; }
}
loadStats();
loadRecentTrips();
loadRecentVideos();
</script>
{% endblock %}