Dashboard: auto-focus running camera, show date + all-camera totals

- Auto-detect active camera by polling frame_index
- Show All Cameras Total accumulation in sidebar
- Show per-camera totals in camera list
- Add run_date to stats.json and display in dashboard
- Pipeline: thread run_date through build/run/batch
This commit is contained in:
proitlab committed 2026-07-22 13:36:20 +07:00
1 parent a54d0e6b2c
commit 6dbaa517dc
3 files changed
+71 -10

No files matched your search

+61 -6
View File
@@ -62,6 +62,8 @@ body{font-family:system-ui,monospace;background:#0f0f14;color:#e0e0e0;overflow:h
<div id="app">
<div id="sidebar">
<h1>&#x1f414; Chicken Counter</h1>
<div class="stat"><label>All Cameras Total</label><div class="value good" id="stat-all-total">--</div></div>
<div class="stat"><label>Active Camera</label><div class="value" id="stat-cam-name">--</div></div>
<div class="stat"><label>Total Entered</label><div class="value good" id="stat-total">--</div></div>
<div class="stat"><label>Inside Box</label><div class="value" id="stat-inside">--</div></div>
<div class="stat"><label>Tracks</label><div class="value" id="stat-tracks">--</div></div>
@@ -73,7 +75,8 @@ body{font-family:system-ui,monospace;background:#0f0f14;color:#e0e0e0;overflow:h
<div id="main">
<div id="top-bar">
<span><span class="status-dot" id="status-dot"></span><span id="status-text">waiting for pipeline...</span></span>
<span><span class="refresh" id="refresh-counter"></span> ago &nbsp;
<span><span id="stat-date">%%DATE%%</span> &nbsp;
<span class="refresh" id="refresh-counter"></span> ago &nbsp;
<button class="refresh-btn" onclick="load()">&#x21bb; Refresh</button></span>
</div>
<div id="frame-container">
@@ -88,6 +91,7 @@ var SHM = "%%SHM_DIR%%";
var cameras = [];
var activeCam = null;
var lastUpdate = 0;
var perCam = {{}};
var img = document.getElementById("frame-img");
var noFrame = document.getElementById("no-frame");
@@ -95,16 +99,52 @@ function loadCameras() {{
fetch("/api/cameras").then(r => r.json()).then(data => {{
cameras = data.cameras || [];
renderCamList();
if (cameras.length && !activeCam) selectCam(cameras[0]);
if (!cameras.length) {{ noFrame.textContent = "No cameras found in " + SHM; img.style.display = "none"; }}
if (!cameras.length) {{ noFrame.textContent = "No cameras found in " + SHM; img.style.display = "none"; updateSidebar(); return; }}
detectActive();
if (!activeCam) selectCam(cameras[0]);
}});
}}
function detectActive() {{
var pending = cameras.length;
cameras.forEach(function(cam) {{
fetch("/shm/" + cam + "/stats.json?t=" + Date.now()).then(function(r) {{
if (!r.ok) return;
return r.json();
}}).then(function(s) {{
pending--;
if (!s) return;
var prev = (perCam[cam] && perCam[cam].frame) || 0;
perCam[cam] = {{
total: s.total_entered_count,
inside: s.inside_box_count,
frame: s.frame_index,
tracks: s.track_count
}};
if (s.frame_index > prev && activeCam !== cam) selectCam(cam);
}}).finally(function() {{
if (pending === 0) updateSidebar();
}});
}});
}}
function updateSidebar() {{
var allTotal = 0;
cameras.forEach(function(cam) {{
if (perCam[cam]) allTotal += perCam[cam].total || 0;
}});
document.getElementById("stat-all-total").textContent = allTotal || "--";
document.getElementById("stat-cam-name").textContent = activeCam || "--";
}}
function renderCamList() {{
var ul = document.getElementById("cam-list");
ul.innerHTML = cameras.map(function(c) {{
var info = perCam[c] || {{}};
var total = info.total || 0;
return '<li class="' + (c === activeCam ? "active" : "") + '" onclick="selectCam(\'' + c + '\')">' +
c + '<span class="cam-badge">&#x25b6;</span></li>';
c + ' <span style="font-size:10px;color:#888">(' + total + ')</span>' +
'<span class="cam-badge">&#x25b6;</span></li>';
}}).join("");
}}
@@ -124,6 +164,15 @@ function load() {{
}}).then(function(s) {{
if (!s) return;
lastUpdate = Date.now();
perCam[activeCam] = {{
total: s.total_entered_count,
inside: s.inside_box_count,
frame: s.frame_index,
tracks: s.track_count
}};
if (s.run_date) {{
document.getElementById("stat-date").textContent = s.run_date;
}}
document.getElementById("stat-total").textContent = s.total_entered_count;
document.getElementById("stat-inside").textContent = s.inside_box_count;
document.getElementById("stat-tracks").textContent = s.track_count;
@@ -134,6 +183,8 @@ function load() {{
el.className = "value" + (s.backward_active ? " warn" : " good");
document.getElementById("status-dot").className = "status-dot online";
document.getElementById("status-text").textContent = activeCam + " \u2022 frame " + s.frame_index;
updateSidebar();
renderCamList();
}});
}}
@@ -162,6 +213,7 @@ loadCameras();
class DashboardHandler(SimpleHTTPRequestHandler):
shm_dir = DEFAULT_SHM_DIR
poll_ms = 500
run_date = ""
def log_message(self, format, *args):
pass
@@ -177,7 +229,7 @@ class DashboardHandler(SimpleHTTPRequestHandler):
path = unquote(parsed.path)
if path == "/" or path == "/index.html":
html = DASHBOARD_HTML.replace("%%POLL_MS%%", str(self.poll_ms)).replace("%%SHM_DIR%%", self.shm_dir)
html = DASHBOARD_HTML.replace("%%POLL_MS%%", str(self.poll_ms)).replace("%%SHM_DIR%%", self.shm_dir).replace("%%DATE%%", self.run_date or "today")
self._send_html(html)
return
@@ -242,14 +294,17 @@ def main():
parser.add_argument("--port", type=int, default=DEFAULT_PORT, help=f"HTTP port (default: {DEFAULT_PORT})")
parser.add_argument("--shm-dir", default=DEFAULT_SHM_DIR, help=f"Shared memory directory (default: {DEFAULT_SHM_DIR})")
parser.add_argument("--poll-ms", type=int, default=500, help="Image poll interval in ms (default: 500)")
parser.add_argument("--date", default="", help="Processing date to display (e.g. 2026-06-10)")
args = parser.parse_args()
DashboardHandler.shm_dir = args.shm_dir
DashboardHandler.poll_ms = args.poll_ms
DashboardHandler.run_date = args.date
server = ThreadingHTTPServer(("0.0.0.0", args.port), DashboardHandler)
date_info = f" date={args.date}" if args.date else ""
print(f"[dashboard] serving at http://0.0.0.0:{args.port}")
print(f"[dashboard] shm_dir={args.shm_dir} poll={args.poll_ms}ms")
print(f"[dashboard] shm_dir={args.shm_dir}{date_info} poll={args.poll_ms}ms")
try:
server.serve_forever()
except KeyboardInterrupt: