waiting for pipeline...
- ago
+%%DATE%%
+ ago
@@ -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 '' +
- c + '▶ ';
+ c + ' (' + total + ')' +
+ '▶';
}}).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:
diff --git a/src/chicken_counter/batch_runner.py b/src/chicken_counter/batch_runner.py
index 5a283d9..cb4ef1a 100755
--- a/src/chicken_counter/batch_runner.py
+++ b/src/chicken_counter/batch_runner.py
@@ -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,
diff --git a/src/chicken_counter/pipeline.py b/src/chicken_counter/pipeline.py
index 86ecb07..7757b0a 100755
--- a/src/chicken_counter/pipeline.py
+++ b/src/chicken_counter/pipeline.py
@@ -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"