diff --git a/SPEC.md b/SPEC.md index 0531c4b..d52e91b 100644 --- a/SPEC.md +++ b/SPEC.md @@ -98,6 +98,7 @@ api: GET /api/live-video → MJPEG stream from shared-memory JPEG api: GET / → recounting HTML api: GET /api/live-progress → proxy to LIVE_API_URL:/api/current-batch api: GET /api/recount-progress → proxy to RECOUNT_API_URL:/api/current-batch +api: GET /api/batch-result?file= → parse `batch_NN_YYYYMMDD_HHmmSS.mp4`, query live API for count api: GET /api/mp4-files → [ {name, path, size, mtime} ... ] api: POST /api/start-recount {path} → configure go2rtc stream, return {stream_url} api: POST /api/stop-recount → tear down go2rtc stream diff --git a/recounting_dashboard.py b/recounting_dashboard.py index 680c114..a5ad8a1 100644 --- a/recounting_dashboard.py +++ b/recounting_dashboard.py @@ -5,6 +5,7 @@ lists OUTPUT_DIR MP4 files, streams via go2rtc for preview. """ import json import os +import re import subprocess import requests from datetime import datetime @@ -34,6 +35,18 @@ FLASK_DEBUG = os.getenv("FLASK_DEBUG", "false").lower() == "true" _http_session = requests.Session() _http_session.timeout = 3 +_FILENAME_RE = re.compile(r"batch_(\d+)_(\d{8})_\d{6}\.mp4") + + +def _parse_filename(filename): + m = _FILENAME_RE.match(filename) + if m: + batch_num = int(m.group(1)) + date_str = m.group(2) + date_formatted = f"{date_str[:4]}-{date_str[4:6]}-{date_str[6:8]}" + return batch_num, date_formatted + return None, None + def _api_get(base_url, path, default=None): try: @@ -107,6 +120,32 @@ def start_recount(): return jsonify({"success": True, "stream_url": stream_url, "file": Path(mp4_path).name}) +@app.route("/api/batch-result") +def api_batch_result(): + fpath = request.args.get("file", "") + filename = Path(fpath).name + batch_num, date_str = _parse_filename(filename) + if batch_num is None: + return jsonify({"success": False, "error": "Cannot parse batch number from filename"}), 200 + + data = _api_get(LIVE_API_URL, f"/api/day-detail/{date_str}") + if not data: + return jsonify({"success": False, "error": "Live API unreachable"}), 200 + + for b in data.get("batches", []): + if b.get("batch_number") == batch_num: + return jsonify({ + "success": True, + "batch_number": batch_num, + "date": date_str, + "count": b["count"], + "start_time": b.get("start_time"), + "end_time": b.get("end_time"), + }) + + return jsonify({"success": False, "error": f"Batch #{batch_num} not found on {date_str}"}), 200 + + @app.route("/api/stop-recount", methods=["POST"]) def stop_recount(): try: diff --git a/requirements.txt b/requirements.txt index f9305e3..2dbf317 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,3 +4,4 @@ opencv-python flask python-dotenv openpyxl +requests diff --git a/templates/recounting.html b/templates/recounting.html index eeaf4e8..2160f18 100644 --- a/templates/recounting.html +++ b/templates/recounting.html @@ -356,6 +356,22 @@
waiting...
+
@@ -467,7 +483,31 @@ function selectFile(path, name) { selectedFile = path; document.getElementById('btn-start').disabled = false; document.getElementById('stream-file-name').textContent = name; - loadFiles(); // refresh selection + loadFiles(); + loadBatchResult(path); +} + +async function loadBatchResult(filePath) { + const el = document.getElementById('batch-result'); + if (!filePath) { + el.style.display = 'none'; + return; + } + try { + const res = await fetch('/api/batch-result?file=' + encodeURIComponent(filePath)); + const data = await res.json(); + if (!data.success) { + el.style.display = 'none'; + return; + } + document.getElementById('br-batch').textContent = '#' + data.batch_number; + document.getElementById('br-count').textContent = data.count.toLocaleString(); + document.getElementById('br-date').textContent = data.date; + el.style.display = 'block'; + } catch (err) { + console.error('Failed to load batch result:', err); + el.style.display = 'none'; + } } function formatSize(bytes) {