forked from zakaria/chicken-counting-sukawarna-det
Today Commit
This commit is contained in:
1 parent
6cddf2c9d3
commit
3b524d4a65
7 files changed
+223
-168
No files matched your search
@@ -6,7 +6,7 @@ After=network.target
|
||||
Type=simple
|
||||
User=dsutanto
|
||||
WorkingDirectory=/media/jetson/DATA/.Codes/chicken-counting-sukawarna-det
|
||||
ExecStart=/media/jetson/DATA/karung-sukawarna/venv/bin/python dashboard.py --port 8080 --db chicken_counts.db
|
||||
ExecStart=/media/jetson/DATA/karung-sukawarna/venv/bin/python dashboard.py --port 8080 --db db/chicken_counts.db
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
Environment=PYTHONUNBUFFERED=1
|
||||
|
||||
@@ -6,7 +6,7 @@ batch:
|
||||
delete_intermediate: false
|
||||
checkpoint_every_n_frames: 3000
|
||||
location: kandang-atas
|
||||
db_path: chicken_counts.db
|
||||
db_path: db/chicken_counts.db
|
||||
|
||||
defaults:
|
||||
detection:
|
||||
|
||||
+191
-151
@@ -1,37 +1,43 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Live dashboard for chicken-counter pipeline using Flask.
|
||||
|
||||
Serve from project root:
|
||||
python3 dashboard.py [--port 8080] [--date 2026-06-10] [--db chicken_counts.db]
|
||||
"""
|
||||
"""Live dashboard for chicken-counter pipeline."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import logging
|
||||
import sqlite3
|
||||
import threading
|
||||
import time
|
||||
from http.server import HTTPServer, SimpleHTTPRequestHandler
|
||||
from pathlib import Path
|
||||
from socketserver import ThreadingMixIn
|
||||
from urllib.parse import unquote, urlparse
|
||||
|
||||
from flask import Flask, jsonify, render_template, send_file
|
||||
from waitress import serve
|
||||
|
||||
# Suppress Flask request logs
|
||||
log = logging.getLogger("werkzeug")
|
||||
log.setLevel(logging.ERROR)
|
||||
class ThreadingHTTPServer(ThreadingMixIn, HTTPServer):
|
||||
daemon_threads = True
|
||||
|
||||
|
||||
DEFAULT_SHM_DIR = "/dev/shm"
|
||||
DEFAULT_PORT = 8080
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config["shm_dir"] = DEFAULT_SHM_DIR
|
||||
app.config["poll_ms"] = 1000
|
||||
app.config["run_date"] = ""
|
||||
app.config["db_path"] = ""
|
||||
TEMPLATE_DIR = Path(__file__).resolve().parent / "templates"
|
||||
|
||||
_db_conn = None
|
||||
_db_lock = threading.Lock()
|
||||
_db_path = ""
|
||||
|
||||
|
||||
def _get_db():
|
||||
global _db_conn
|
||||
if not _db_path:
|
||||
return None
|
||||
with _db_lock:
|
||||
if _db_conn is None:
|
||||
_db_conn = sqlite3.connect(_db_path, check_same_thread=False)
|
||||
_db_conn.row_factory = sqlite3.Row
|
||||
_db_conn.execute("PRAGMA journal_mode=WAL")
|
||||
_db_conn.execute("PRAGMA cache_size=-8000")
|
||||
return _db_conn
|
||||
|
||||
|
||||
def _init_db(db_path: str) -> None:
|
||||
@@ -51,22 +57,8 @@ def _init_db(db_path: str) -> None:
|
||||
conn.close()
|
||||
|
||||
|
||||
def _get_db():
|
||||
global _db_conn
|
||||
db = app.config["db_path"]
|
||||
if not db:
|
||||
return None
|
||||
with _db_lock:
|
||||
if _db_conn is None:
|
||||
_db_conn = sqlite3.connect(db, check_same_thread=False)
|
||||
_db_conn.row_factory = sqlite3.Row
|
||||
_db_conn.execute("PRAGMA journal_mode=WAL")
|
||||
_db_conn.execute("PRAGMA cache_size=-8000")
|
||||
return _db_conn
|
||||
|
||||
|
||||
def _discover_cameras():
|
||||
shm = Path(app.config["shm_dir"])
|
||||
def _discover_cameras(shm_dir):
|
||||
shm = Path(shm_dir)
|
||||
cameras = []
|
||||
if shm.is_dir():
|
||||
for entry in sorted(shm.iterdir()):
|
||||
@@ -75,152 +67,200 @@ def _discover_cameras():
|
||||
return cameras
|
||||
|
||||
|
||||
@app.route("/")
|
||||
def index():
|
||||
return render_template(
|
||||
"index.html",
|
||||
poll_ms=app.config["poll_ms"],
|
||||
shm_dir=app.config["shm_dir"],
|
||||
date=app.config["run_date"] or "today",
|
||||
db_path=app.config["db_path"],
|
||||
)
|
||||
class DashboardHandler(SimpleHTTPRequestHandler):
|
||||
shm_dir = DEFAULT_SHM_DIR
|
||||
poll_ms = 1000
|
||||
run_date = ""
|
||||
|
||||
def log_message(self, format, *args):
|
||||
pass
|
||||
|
||||
@app.route("/api/cameras")
|
||||
def api_cameras():
|
||||
return jsonify({"cameras": _discover_cameras()})
|
||||
def do_GET(self):
|
||||
try:
|
||||
self._handle()
|
||||
except (BrokenPipeError, ConnectionResetError):
|
||||
pass
|
||||
|
||||
def _handle(self):
|
||||
parsed = urlparse(self.path)
|
||||
path = unquote(parsed.path)
|
||||
|
||||
@app.route("/shm/<camera_id>/stats.json")
|
||||
def shm_stats(camera_id):
|
||||
stats_path = Path(app.config["shm_dir"]) / f"chicken_counter_{camera_id}" / "stats.json"
|
||||
if not stats_path.exists():
|
||||
return jsonify({"error": "not found"}), 404
|
||||
return jsonify(json.loads(stats_path.read_text()))
|
||||
if path == "/":
|
||||
self._serve_html()
|
||||
return
|
||||
|
||||
if path.startswith("/stream/"):
|
||||
self._handle_stream(path)
|
||||
return
|
||||
|
||||
@app.route("/shm/<camera_id>/frame.jpg")
|
||||
def shm_frame(camera_id):
|
||||
frame_path = Path(app.config["shm_dir"]) / f"chicken_counter_{camera_id}" / "frame.jpg"
|
||||
if not frame_path.exists():
|
||||
return jsonify({"error": "not found"}), 404
|
||||
try:
|
||||
return send_file(frame_path, mimetype="image/jpeg", max_age=2, download_name=None)
|
||||
except (FileNotFoundError, PermissionError):
|
||||
return jsonify({"error": "not ready"}), 503
|
||||
if path == "/api/cameras":
|
||||
self._send_json({"cameras": _discover_cameras(self.shm_dir)})
|
||||
return
|
||||
|
||||
if path.startswith("/api/db/"):
|
||||
self._handle_db(path)
|
||||
return
|
||||
|
||||
def _get_db():
|
||||
db = app.config["db_path"]
|
||||
if not db or not Path(db).exists():
|
||||
return None
|
||||
conn = sqlite3.connect(db)
|
||||
conn.row_factory = sqlite3.Row
|
||||
return conn
|
||||
if path.startswith("/shm/"):
|
||||
self._handle_shm(path)
|
||||
return
|
||||
|
||||
self._send_error(404)
|
||||
|
||||
@app.route("/api/db/summary")
|
||||
def db_summary():
|
||||
conn = _get_db()
|
||||
if not conn:
|
||||
return jsonify({})
|
||||
row = conn.execute("""
|
||||
SELECT COUNT(DISTINCT date) AS days,
|
||||
COUNT(DISTINCT location) AS locations,
|
||||
COUNT(*) AS total_runs,
|
||||
SUM(total_entered) AS total_chickens,
|
||||
ROUND(SUM(elapsed_seconds)/3600.0, 1) AS total_hours
|
||||
FROM batch_runs
|
||||
""").fetchone()
|
||||
return jsonify(dict(row))
|
||||
def _handle_stream(self, path):
|
||||
cam_id = path[len("/stream/"):]
|
||||
frame_path = Path(self.shm_dir) / f"chicken_counter_{cam_id}" / "frame.jpg"
|
||||
if not frame_path.exists():
|
||||
self._send_error(404)
|
||||
return
|
||||
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "multipart/x-mixed-replace; boundary=frame")
|
||||
self.send_header("Cache-Control", "no-cache")
|
||||
self.end_headers()
|
||||
|
||||
@app.route("/api/db/history")
|
||||
def db_history():
|
||||
conn = _get_db()
|
||||
if not conn:
|
||||
return jsonify([])
|
||||
rows = conn.execute("""
|
||||
SELECT date, location,
|
||||
COUNT(*) AS cams,
|
||||
SUM(total_entered) AS total,
|
||||
ROUND(SUM(elapsed_seconds)/60.0, 1) AS minutes
|
||||
FROM batch_runs
|
||||
GROUP BY date, location
|
||||
ORDER BY date DESC, location
|
||||
LIMIT 50
|
||||
""").fetchall()
|
||||
return jsonify([dict(r) for r in rows])
|
||||
last_mtime = 0
|
||||
try:
|
||||
while True:
|
||||
try:
|
||||
mtime = frame_path.stat().st_mtime
|
||||
if mtime != last_mtime:
|
||||
last_mtime = mtime
|
||||
data = frame_path.read_bytes()
|
||||
self.wfile.write(
|
||||
b"--frame\r\n"
|
||||
b"Content-Type: image/jpeg\r\n"
|
||||
b"Content-Length: " + str(len(data)).encode() + b"\r\n\r\n" +
|
||||
data + b"\r\n"
|
||||
)
|
||||
self.wfile.flush()
|
||||
except (FileNotFoundError, OSError):
|
||||
pass
|
||||
time.sleep(0.1)
|
||||
except (BrokenPipeError, ConnectionResetError):
|
||||
pass
|
||||
|
||||
def _handle_shm(self, path):
|
||||
rel = path[len("/shm/"):]
|
||||
parts = rel.split("/", 1)
|
||||
if len(parts) < 2:
|
||||
self._send_error(404)
|
||||
return
|
||||
|
||||
@app.route("/api/db/date/<date>")
|
||||
def db_date(date):
|
||||
conn = _get_db()
|
||||
if not conn:
|
||||
return jsonify({})
|
||||
cameras = conn.execute("""
|
||||
SELECT camera_id, total_entered, frames_processed,
|
||||
ROUND(elapsed_seconds,1) AS elapsed_seconds,
|
||||
stopped_reason, source_video, location
|
||||
FROM batch_runs WHERE date=? ORDER BY camera_id
|
||||
""", (date,)).fetchall()
|
||||
total = conn.execute(
|
||||
"SELECT SUM(total_entered) AS total, ROUND(SUM(elapsed_seconds)/60.0,1) AS minutes "
|
||||
"FROM batch_runs WHERE date=?", (date,)).fetchone()
|
||||
return jsonify({"date": date, "total": dict(total), "cameras": [dict(r) for r in cameras]})
|
||||
cam_id = parts[0]
|
||||
file = parts[1]
|
||||
fpath = Path(self.shm_dir) / f"chicken_counter_{cam_id}" / file
|
||||
|
||||
if str(fpath.resolve()).startswith(str(Path(self.shm_dir).resolve())):
|
||||
if fpath.exists():
|
||||
ct = "image/jpeg" if file.endswith(".jpg") else "application/json"
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", ct)
|
||||
self.send_header("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
self.end_headers()
|
||||
self.wfile.write(fpath.read_bytes())
|
||||
return
|
||||
self._send_error(404)
|
||||
|
||||
@app.route("/api/db/camera/<camera_id>")
|
||||
def db_camera(camera_id):
|
||||
conn = _get_db()
|
||||
if not conn:
|
||||
return jsonify([])
|
||||
rows = conn.execute("""
|
||||
SELECT date, location, total_entered, frames_processed,
|
||||
ROUND(elapsed_seconds,1) AS elapsed_seconds, stopped_reason
|
||||
FROM batch_runs WHERE camera_id=? ORDER BY date DESC LIMIT 50
|
||||
""", (camera_id,)).fetchall()
|
||||
return jsonify([dict(r) for r in rows])
|
||||
def _handle_db(self, path):
|
||||
conn = _get_db()
|
||||
if not conn:
|
||||
self._send_json({})
|
||||
return
|
||||
|
||||
if path == "/api/db/summary":
|
||||
row = conn.execute("SELECT COUNT(DISTINCT date) AS days, COUNT(DISTINCT location) AS locations, COUNT(*) AS total_runs, SUM(total_entered) AS total_chickens, ROUND(SUM(elapsed_seconds)/3600.0,1) AS total_hours FROM batch_runs").fetchone()
|
||||
self._send_json(dict(row))
|
||||
return
|
||||
|
||||
@app.route("/api/db/location/<location>")
|
||||
def db_location(location):
|
||||
conn = _get_db()
|
||||
if not conn:
|
||||
return jsonify({})
|
||||
history = conn.execute("""
|
||||
SELECT date, GROUP_CONCAT(camera_id,', ') AS cameras,
|
||||
SUM(total_entered) AS total,
|
||||
ROUND(SUM(elapsed_seconds)/60.0,1) AS minutes
|
||||
FROM batch_runs WHERE location=? GROUP BY date ORDER BY date DESC LIMIT 50
|
||||
""", (location,)).fetchall()
|
||||
summary = conn.execute("""
|
||||
SELECT COUNT(DISTINCT date) AS days, SUM(total_entered) AS total,
|
||||
ROUND(SUM(elapsed_seconds)/3600.0,1) AS hours
|
||||
FROM batch_runs WHERE location=?
|
||||
""", (location,)).fetchone()
|
||||
return jsonify({"location": location, "summary": dict(summary), "history": [dict(r) for r in history]})
|
||||
if path == "/api/db/history":
|
||||
rows = conn.execute("SELECT date, location, COUNT(*) AS cams, SUM(total_entered) AS total, ROUND(SUM(elapsed_seconds)/60.0,1) AS minutes FROM batch_runs GROUP BY date, location ORDER BY date DESC, location LIMIT 50").fetchall()
|
||||
self._send_json([dict(r) for r in rows])
|
||||
return
|
||||
|
||||
# /api/db/date/<date>
|
||||
prefix = "/api/db/date/"
|
||||
if path.startswith(prefix):
|
||||
date = path[len(prefix):]
|
||||
cameras = conn.execute("SELECT camera_id, total_entered, frames_processed, ROUND(elapsed_seconds,1) AS elapsed_seconds, stopped_reason, source_video, location FROM batch_runs WHERE date=? ORDER BY camera_id", (date,)).fetchall()
|
||||
total = conn.execute("SELECT SUM(total_entered) AS total, ROUND(SUM(elapsed_seconds)/60.0,1) AS minutes FROM batch_runs WHERE date=?", (date,)).fetchone()
|
||||
self._send_json({"date": date, "total": dict(total), "cameras": [dict(r) for r in cameras]})
|
||||
return
|
||||
|
||||
# /api/db/camera/<id>
|
||||
prefix = "/api/db/camera/"
|
||||
if path.startswith(prefix):
|
||||
cam_id = path[len(prefix):]
|
||||
rows = conn.execute("SELECT date, location, total_entered, frames_processed, ROUND(elapsed_seconds,1) AS elapsed_seconds, stopped_reason FROM batch_runs WHERE camera_id=? ORDER BY date DESC LIMIT 50", (cam_id,)).fetchall()
|
||||
self._send_json([dict(r) for r in rows])
|
||||
return
|
||||
|
||||
# /api/db/location/<name>
|
||||
prefix = "/api/db/location/"
|
||||
if path.startswith(prefix):
|
||||
loc = path[len(prefix):]
|
||||
history = conn.execute("SELECT date, GROUP_CONCAT(camera_id,', ') AS cameras, SUM(total_entered) AS total, ROUND(SUM(elapsed_seconds)/60.0,1) AS minutes FROM batch_runs WHERE location=? GROUP BY date ORDER BY date DESC LIMIT 50", (loc,)).fetchall()
|
||||
summary = conn.execute("SELECT COUNT(DISTINCT date) AS days, SUM(total_entered) AS total, ROUND(SUM(elapsed_seconds)/3600.0,1) AS hours FROM batch_runs WHERE location=?", (loc,)).fetchone()
|
||||
self._send_json({"location": loc, "summary": dict(summary), "history": [dict(r) for r in history]})
|
||||
return
|
||||
|
||||
self._send_json({})
|
||||
|
||||
def _serve_html(self):
|
||||
html_path = TEMPLATE_DIR / "index.html"
|
||||
html = html_path.read_text(encoding="utf-8")
|
||||
html = html.replace("{{ poll_ms }}", str(self.poll_ms))
|
||||
html = html.replace("{{ shm_dir }}", self.shm_dir)
|
||||
html = html.replace("{{ date }}", self.run_date or "today")
|
||||
html = html.replace("{{ db_path }}", _db_path)
|
||||
data = html.encode("utf-8")
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "text/html; charset=utf-8")
|
||||
self.send_header("Content-Length", str(len(data)))
|
||||
self.end_headers()
|
||||
self.wfile.write(data)
|
||||
|
||||
def _send_json(self, obj):
|
||||
data = json.dumps(obj).encode("utf-8")
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", str(len(data)))
|
||||
self.end_headers()
|
||||
self.wfile.write(data)
|
||||
|
||||
def _send_error(self, code):
|
||||
self.send_response(code)
|
||||
self.send_header("Content-Length", "0")
|
||||
self.end_headers()
|
||||
|
||||
|
||||
def main():
|
||||
global _db_path
|
||||
|
||||
parser = argparse.ArgumentParser(description="Chicken Counter live dashboard")
|
||||
parser.add_argument("--port", type=int, default=DEFAULT_PORT)
|
||||
parser.add_argument("--shm-dir", default=DEFAULT_SHM_DIR)
|
||||
parser.add_argument("--poll-ms", type=int, default=500)
|
||||
parser.add_argument("--date", default="", help="Processing date")
|
||||
parser.add_argument("--db", default="chicken_counts.db", help="SQLite database path")
|
||||
parser.add_argument("--poll-ms", type=int, default=1000)
|
||||
parser.add_argument("--date", default="")
|
||||
parser.add_argument("--db", default="db/chicken_counts.db")
|
||||
args = parser.parse_args()
|
||||
|
||||
app.config["shm_dir"] = args.shm_dir
|
||||
app.config["poll_ms"] = args.poll_ms
|
||||
app.config["run_date"] = args.date
|
||||
app.config["db_path"] = str(Path(args.db).resolve()) if args.db else ""
|
||||
DashboardHandler.shm_dir = args.shm_dir
|
||||
DashboardHandler.poll_ms = args.poll_ms
|
||||
DashboardHandler.run_date = args.date
|
||||
_db_path = str(Path(args.db).resolve()) if args.db else ""
|
||||
|
||||
if app.config["db_path"]:
|
||||
_init_db(app.config["db_path"])
|
||||
if _db_path:
|
||||
_init_db(_db_path)
|
||||
|
||||
print(f"[dashboard] http://0.0.0.0:{args.port} shm={args.shm_dir} db={args.db}" + (f" date={args.date}" if args.date else ""))
|
||||
serve(app, host="0.0.0.0", port=args.port, threads=8)
|
||||
server = ThreadingHTTPServer(("0.0.0.0", args.port), DashboardHandler)
|
||||
date_info = f" date={args.date}" if args.date else ""
|
||||
print(f"[dashboard] http://0.0.0.0:{args.port} shm={args.shm_dir} db={args.db}{date_info}")
|
||||
try:
|
||||
server.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
print("\n[dashboard] stopped")
|
||||
server.server_close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Whitespace-only changes.
+30
-15
@@ -69,6 +69,7 @@ body{font-family:system-ui,monospace;background:#0f0f14;color:#e0e0e0;overflow:h
|
||||
var POLL_MS = {{ poll_ms }};
|
||||
var SHM = "{{ shm_dir }}";
|
||||
var DB_PATH = "{{ db_path }}";
|
||||
var RUN_DATE = "{{ date }}";
|
||||
var cameras = [], activeCam = null, lastUpdate = 0;
|
||||
var perCam = {}, lastFramePerCam = {};
|
||||
|
||||
@@ -114,6 +115,7 @@ function selectCam(id) {
|
||||
document.getElementById("waiting-msg").style.display = "none";
|
||||
document.getElementById("frame-overlay").style.display = "block";
|
||||
document.getElementById("frame-img").style.display = "block";
|
||||
document.getElementById("frame-img").src = "/stream/" + id;
|
||||
renderCamList();
|
||||
refreshNow();
|
||||
}
|
||||
@@ -124,7 +126,6 @@ function poll() {
|
||||
fetch("/shm/" + activeCam + "/stats.json?t=" + t).then(r => r.ok ? r.json() : null).then(s => {
|
||||
if (!s) { setOffline(); return; }
|
||||
lastUpdate = Date.now();
|
||||
var oldFrame = lastFramePerCam[activeCam] || 0;
|
||||
lastFramePerCam[activeCam] = s.frame_index;
|
||||
perCam[activeCam] = { total: s.total_entered_count, inside: s.inside_box_count, frame: s.frame_index, tracks: s.track_count, speed: s.smoothed_speed };
|
||||
document.getElementById("s-total").textContent = s.total_entered_count;
|
||||
@@ -137,18 +138,11 @@ function poll() {
|
||||
tag.textContent = activeCam + " \u2022 " + status;
|
||||
tag.className = "tag " + (s.backward_active ? "off" : "live");
|
||||
document.getElementById("cam-count").textContent = s.total_entered_count;
|
||||
// only refresh frame when index changes (every 5 frames with interval_frames=5)
|
||||
if (s.frame_index != oldFrame) {
|
||||
document.getElementById("frame-img").src = "/shm/" + activeCam + "/frame.jpg?t=" + t;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function refreshNow() {
|
||||
poll();
|
||||
// also update the frame immediately
|
||||
var t = Date.now();
|
||||
document.getElementById("frame-img").src = "/shm/" + activeCam + "/frame.jpg?t=" + t;
|
||||
}
|
||||
|
||||
function showWaiting(msg) {
|
||||
@@ -163,6 +157,8 @@ function setOffline() {
|
||||
document.getElementById("cam-tag").className = "tag off";
|
||||
}
|
||||
|
||||
function updateSidebar() {}
|
||||
|
||||
function renderCamList() {
|
||||
var ul = document.getElementById("cam-list");
|
||||
ul.innerHTML = cameras.map(function(c) {
|
||||
@@ -180,15 +176,34 @@ function updateClock() {
|
||||
|
||||
function loadHistory() {
|
||||
if (!DB_PATH) return;
|
||||
var panel = document.getElementById("db-panel");
|
||||
panel.style.display = "block";
|
||||
|
||||
// per-day totals
|
||||
fetch("/api/db/history").then(r => r.json()).then(rows => {
|
||||
var panel = document.getElementById("db-panel");
|
||||
if (!rows.length) { panel.style.display = "none"; return; }
|
||||
panel.style.display = "block";
|
||||
panel.innerHTML = "<h3>📊 History</h3>" + rows.map(function(r) {
|
||||
return '<div class="db-row"><span>' + r.date + ' ' + r.location + '</span>' +
|
||||
'<span class="db-total">' + r.total.toLocaleString() + '</span></div>';
|
||||
}).join("");
|
||||
var html = "<h3>📊 History</h3>";
|
||||
if (rows.length) {
|
||||
html += rows.map(function(r) {
|
||||
return '<div class="db-row"><span>' + r.date + ' ' + r.location + '</span>' +
|
||||
'<span class="db-total">' + r.total.toLocaleString() + '</span></div>';
|
||||
}).join("");
|
||||
}
|
||||
panel.innerHTML = html;
|
||||
});
|
||||
|
||||
// per-camera breakdown for current date
|
||||
if (RUN_DATE && RUN_DATE !== "today") {
|
||||
fetch("/api/db/date/" + RUN_DATE).then(r => r.json()).then(data => {
|
||||
if (!data.cameras || !data.cameras.length) return;
|
||||
var html = panel.innerHTML;
|
||||
html += '<h3 style="margin-top:12px">📹 ' + RUN_DATE + ' (' + (data.total.total || 0).toLocaleString() + ')</h3>';
|
||||
data.cameras.forEach(function(c) {
|
||||
html += '<div class="db-row"><span>' + c.camera_id + '</span>' +
|
||||
'<span class="db-total">' + c.total_entered.toLocaleString() + '</span></div>';
|
||||
});
|
||||
panel.innerHTML = html;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(function() { if (activeCam) refreshNow(); }, 500);
|
||||
|
||||
Reference in new issue
Block a user