diff --git a/app.py b/app.py index 10f1bed..a431cdd 100644 --- a/app.py +++ b/app.py @@ -255,6 +255,10 @@ def api_dtc(): "codes": app_obd.get_fresh_dtc_codes(), }) +@app.route('/api/stats') +def api_stats(): + return jsonify(app_sql.get_overview_stats()) + if __name__ == '__main__': # Debug/Development # app.run(debug=True, host="0.0.0.0", port="5000") diff --git a/modules/app_sql.py b/modules/app_sql.py index d5ed7a0..b6629e9 100644 --- a/modules/app_sql.py +++ b/modules/app_sql.py @@ -317,6 +317,32 @@ def refresh_trips(force=False): return conn.execute("SELECT COUNT(*) FROM trips").fetchone()[0] +def get_overview_stats(): + conn = get_conn() + with _conn_lock: + row = conn.execute(""" + SELECT COUNT(*), COALESCE(SUM(DISTANCE_KM),0), + COALESCE(SUM(DURATION_SECONDS),0), COALESCE(MAX(MAX_SPEED_KMH),0) + FROM trips + """).fetchone() + dtc_row = conn.execute(""" + SELECT COUNT(*) FROM dtc_codes WHERE cleared_at IS NULL + """).fetchone() if _table_exists(conn, "dtc_codes") else (0,) + return { + "trip_count": row[0], + "total_km": row[1], + "total_duration_s": row[2], + "all_time_max_speed": row[3], + "active_dtc": dtc_row[0] if dtc_row else 0, + } + + +def _table_exists(conn, name): + return conn.execute( + "SELECT 1 FROM sqlite_master WHERE type='table' AND name=?", (name,) + ).fetchone() is not None + + def list_trips(): refresh_trips() conn = get_conn() diff --git a/static/js/local-videos.js b/static/js/local-videos.js index a90aba2..22e0cc9 100644 --- a/static/js/local-videos.js +++ b/static/js/local-videos.js @@ -228,142 +228,4 @@ return cell; } - function closePlayer() { - player.pause(); - player.removeAttribute("src"); - player.load(); - playerPanel.hidden = true; - playingPath = null; - } - function playVideo(path) { - playingPath = path; - playerTitle.textContent = path; - player.src = `/api/local-video/file?path=${encodeURIComponent(path)}`; - playerPanel.hidden = false; - player.load(); - player.play().catch(function () { - // Browser autoplay policy may require the user to press play. - }); - playerPanel.scrollIntoView({ behavior: "smooth", block: "start" }); - } - - function renderFiles(result) { - thumbnailObserver.disconnect(); - body.replaceChildren(); - const files = result.files || []; - let uploadedCount = 0; - - files.forEach(function (file) { - const row = document.createElement("tr"); - row.dataset.path = file.path; - row.dataset.size = file.size; - row.dataset.modified = file.modified_at; - row.dataset.uploaded = file.uploaded ? "1" : "0"; - - const preview = document.createElement("td"); - const image = document.createElement("img"); - image.className = "local-video-thumbnail"; - image.alt = `Preview of ${file.path}`; - image.loading = "lazy"; - image.dataset.path = file.path; - image.addEventListener("error", function () { - image.hidden = true; - }); - preview.appendChild(image); - row.appendChild(preview); - thumbnailObserver.observe(image); - - row.appendChild(textCell(file.path)); - row.appendChild(textCell(YarisUI.formatBytes(file.size))); - row.appendChild(textCell(new Date(file.modified_at * 1000).toLocaleString())); - - const upload = document.createElement("td"); - const badge = document.createElement("span"); - badge.className = file.uploaded ? "badge bg-success" : "badge bg-warning text-dark"; - badge.textContent = file.uploaded ? "Uploaded" : "Pending"; - upload.appendChild(badge); - row.appendChild(upload); - if (file.uploaded) { - uploadedCount += 1; - } - - const actions = document.createElement("td"); - actions.className = "data-list-actions"; - const playButton = document.createElement("button"); - playButton.type = "button"; - playButton.className = "btn btn-sm btn-outline-primary"; - playButton.dataset.playPath = file.path; - playButton.innerHTML = 'Play'; - actions.appendChild(playButton); - - const deleteButton = document.createElement("button"); - deleteButton.type = "button"; - deleteButton.className = "btn btn-sm btn-outline-danger"; - deleteButton.dataset.deleteLocalPath = file.path; - deleteButton.dataset.confirm = `Delete the local Pi copy of "${file.path}"? The remote archive will not be changed.`; - deleteButton.innerHTML = 'Delete local'; - actions.appendChild(deleteButton); - row.appendChild(actions); - body.appendChild(row); - }); - - summary.textContent = `${files.length} video(s), ${YarisUI.formatBytes(result.total_size || 0)} total, ${uploadedCount} uploaded`; - if (result.retention_days > 0) { - summary.textContent += `; uploaded copies older than ${result.retention_days} day(s) are auto-deleted`; - } else { - summary.textContent += "; automatic retention cleanup is off"; - } - if (result.errors && result.errors.length) { - summary.textContent += ` (${result.errors.length} file error(s))`; - } - } - - function refreshFiles() { - refreshButton.disabled = true; - summary.textContent = "Loading..."; - return fetchJson("/api/local-videos") - .then(renderFiles) - .catch(function (error) { - body.replaceChildren(); - summary.textContent = `Unable to list local videos: ${error.message}`; - }) - .finally(function () { - refreshButton.disabled = false; - }); - } - - function deleteVideo(path) { - return fetchJson("/api/local-video", { - method: "DELETE", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ path }), - }).then(function () { - if (playingPath === path) { - closePlayer(); - } - return refreshFiles(); - }).catch(function (error) { - summary.textContent = `Local delete failed: ${error.message}`; - }); - } - - body.addEventListener("click", function (event) { - const playButton = event.target.closest("[data-play-path]"); - if (playButton) { - playVideo(playButton.dataset.playPath); - return; - } - - const deleteButton = event.target.closest("[data-delete-local-path]"); - if (deleteButton) { - deleteButton.disabled = true; - deleteVideo(deleteButton.dataset.deleteLocalPath).finally(function () { - deleteButton.disabled = false; - }); - } - }); - refreshButton.addEventListener("click", refreshFiles); - closePlayerButton.addEventListener("click", closePlayer); - refreshFiles(); -}());