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();
-}());