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:
1 parent
a54d0e6b2c
commit
6dbaa517dc
3 files changed
+71
-10
No files matched your search
+61
-6
@@ -62,6 +62,8 @@ body{font-family:system-ui,monospace;background:#0f0f14;color:#e0e0e0;overflow:h
|
||||
<div id="app">
|
||||
<div id="sidebar">
|
||||
<h1>🐔 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
|
||||
<span><span id="stat-date">%%DATE%%</span>
|
||||
<span class="refresh" id="refresh-counter"></span> ago
|
||||
<button class="refresh-btn" onclick="load()">↻ 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">▶</span></li>';
|
||||
c + ' <span style="font-size:10px;color:#888">(' + total + ')</span>' +
|
||||
'<span class="cam-badge">▶</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:
|
||||
|
||||
Reference in new issue
Block a user