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:
+1 -1
View File
@@ -58,7 +58,7 @@ def run_daily_batch(settings: BatchSettings, date: str | None = None, *, verbose
checkpoint_dir=checkpoint_dir,
)
camera_config.performance.verbose = verbose
pipeline_result = run_pipeline(camera_config, show_progress=show_progress)
pipeline_result = run_pipeline(camera_config, show_progress=show_progress, run_date=run_date)
camera_results.append(
CameraBatchResult(
camera_id=camera_id,
+9 -3
View File
@@ -90,11 +90,14 @@ class PipelineArtifacts:
total_source_frames: int | None
owns_tracker: bool
detection_zone_rect: tuple[int, int, int, int] | None = None
run_date: str = ""
def build_pipeline(
config: CameraConfig,
tracker: DetectionTracker | None = None,
*,
run_date: str = "",
) -> PipelineArtifacts:
capture = open_capture(config.source)
owns_tracker = tracker is None
@@ -157,6 +160,7 @@ def build_pipeline(
total_source_frames=total_source_frames,
owns_tracker=owns_tracker,
detection_zone_rect=detection_zone_rect,
run_date=run_date,
)
@@ -165,12 +169,13 @@ def run_pipeline(
tracker: DetectionTracker | None = None,
*,
show_progress: bool = False,
run_date: str = "",
) -> PipelineResult:
if tracker is not None:
tracker.config = config
tracker.reset_tracking()
artifacts = build_pipeline(config, tracker=tracker)
artifacts = build_pipeline(config, tracker=tracker, run_date=run_date)
inference_stride = max(1, config.performance.inference_stride)
print(
f"[perf] inference_stride={inference_stride} "
@@ -252,7 +257,7 @@ def run_pipeline(
last_annotated = annotated
if config.stream.enabled and frame_index % max(1, config.stream.interval_frames) == 0:
_write_stream_frame(config.stream.shm_dir, config.camera_id, annotated, result)
_write_stream_frame(config.stream.shm_dir, config.camera_id, annotated, result, run_date=artifacts.run_date)
if verbose:
t_write = time.monotonic()
@@ -366,7 +371,7 @@ def _consume_result(
_emit_periodic_feedback(config, artifacts, annotated, result, progress)
def _write_stream_frame(shm_dir: str, camera_id: str, frame: np.ndarray, result: FrameResult) -> None:
def _write_stream_frame(shm_dir: str, camera_id: str, frame: np.ndarray, result: FrameResult, *, run_date: str = "") -> None:
cam_dir = Path(shm_dir) / f"chicken_counter_{camera_id}"
cam_dir.mkdir(parents=True, exist_ok=True)
@@ -383,6 +388,7 @@ def _write_stream_frame(shm_dir: str, camera_id: str, frame: np.ndarray, result:
"backward_active": result.motion_state.backward_active,
"smoothed_speed": round(result.motion_state.smoothed_speed, 1),
"count_events": len(result.count_events),
"run_date": run_date,
}
stats_path = cam_dir / "stats.json"
stats_tmp = cam_dir / ".stats_tmp.json"