From ba712bd6dbedd158a0b5b17bc29369c448ad827e Mon Sep 17 00:00:00 2001 From: Mathieu Moran Date: Mon, 10 Aug 2026 01:23:53 -0400 Subject: [PATCH] fix(dashcam): prevent site freeze during import/convert - _convert_bg now runs in real threading.Thread (not gevent greenlet) so ffmpeg proc.wait() doesn't block gevent event loop - _device_lock released before copy loop; only held for mount/unmount so /api/dash/files and other routes don't block during long copies - Date now shows time (Aug 9, 10:30 PM) on mobile video rows - Spacing added above converting spinner badge Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- modules/app_dash.py | 24 +++++++++++++----------- modules/app_video.py | 8 +++++--- static/js/local-videos.js | 2 +- static/yaris.css | 4 ++-- 4 files changed, 21 insertions(+), 17 deletions(-) diff --git a/modules/app_dash.py b/modules/app_dash.py index ecf65e4..a301d75 100644 --- a/modules/app_dash.py +++ b/modules/app_dash.py @@ -559,17 +559,19 @@ def import_dashcam( "message": f"Failed to mount {device_path}.", } - try: - summary = copy_files(source, destination_folder, relative_paths) - summary["status"] = ( - "invalid_selection" if summary.pop("invalid_selection", False) else "ok" - ) - if summary["status"] == "invalid_selection": - summary["message"] = summary["errors"][0] - summary["device"] = device_path - return summary - finally: - if mounted_here: + # Lock released — copy runs without blocking other requests + try: + summary = copy_files(source, destination_folder, relative_paths) + summary["status"] = ( + "invalid_selection" if summary.pop("invalid_selection", False) else "ok" + ) + if summary["status"] == "invalid_selection": + summary["message"] = summary["errors"][0] + summary["device"] = device_path + return summary + finally: + if mounted_here: + with _device_lock: unmount_device(mount_point) diff --git a/modules/app_video.py b/modules/app_video.py index 17f2d2f..e6ba2c5 100644 --- a/modules/app_video.py +++ b/modules/app_video.py @@ -2,6 +2,7 @@ import os import subprocess import time +import threading import gevent from gevent import sleep @@ -35,7 +36,9 @@ def queue_convert(relative_path): if relative_path in _converting: return _converting.add(relative_path) - gevent.spawn(_convert_bg, relative_path) + gevent.sleep(0) # yield before spawning thread + t = threading.Thread(target=_convert_bg, args=(relative_path,), daemon=True) + t.start() def _convert_bg(relative_path): @@ -59,8 +62,7 @@ def _convert_bg(relative_path): tmp_path, ] proc = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - while proc.poll() is None: - sleep(1) + proc.wait() # real thread — blocking wait is fine if proc.returncode == 0 and os.path.isfile(tmp_path) and os.path.getsize(tmp_path) > 0: os.replace(tmp_path, mp4_path) # Delete original now that MP4 is ready diff --git a/static/js/local-videos.js b/static/js/local-videos.js index 4f0ae40..0c1198a 100644 --- a/static/js/local-videos.js +++ b/static/js/local-videos.js @@ -115,7 +115,7 @@ // Date shown only on mobile (desktop has its own column) const mobileMeta = document.createElement("div"); mobileMeta.className = "dash-thumb-meta text-muted"; - mobileMeta.textContent = new Date(file.modified_at * 1000).toLocaleDateString(undefined, { month: 'short', day: 'numeric' }); + mobileMeta.textContent = new Date(file.modified_at * 1000).toLocaleString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }); preview.appendChild(mobileMeta); // Converting badge shown on mobile inside thumbnail cell if (file.converting) { diff --git a/static/yaris.css b/static/yaris.css index bde7388..dfb5a51 100644 --- a/static/yaris.css +++ b/static/yaris.css @@ -321,8 +321,8 @@ body { .dash-thumb-name { display: block !important; font-size: .7rem; margin-top: .2rem; } /* Date + converting badge under thumbnail (mobile only) */ - .dash-thumb-meta { display: block; font-size: .65rem; line-height: 1.2; } - .dash-thumb-converting { display: inline-block; font-size: .65rem; padding: .1rem .3rem; } + .dash-thumb-meta { display: block; font-size: .65rem; line-height: 1.2; margin-top: .15rem; } + .dash-thumb-converting { display: inline-block; font-size: .65rem; padding: .1rem .3rem; margin-top: .25rem; } /* Icon-only buttons */ .btn-icon-mobile .btn-label { display: none; }