Files
zenai-apc-salatiga/counter_dashboard.py
T
2026-05-19 14:32:46 +07:00

463 lines
13 KiB
Python

#!/usr/bin/env python3
"""
Frigate Counter Dashboard
A beautiful, interactive Flask web app for viewing daily batch counting data.
"""
import json
import os
import sqlite3
import csv
from io import StringIO
from datetime import datetime, timedelta
from flask import Flask, render_template, jsonify, request, Response
from werkzeug.serving import WSGIRequestHandler
app = Flask(__name__, template_folder="templates")
app.config["SECRET_KEY"] = os.getenv("SECRET_KEY", "frigate-counter-dashboard")
DB_PATH = os.getenv("DB_PATH", "frigate_counter.db")
CURRENT_BATCH_PATH = os.getenv("CURRENT_BATCH_PATH", "current_batch.json")
CUTOFF_TIME = os.getenv("CUTOFF_TIME", "17:00")
# ------------------------------------------------------------------ #
# Database helpers
# ------------------------------------------------------------------ #
def get_db():
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
return conn
# def get_counting_date(dt=None, cutoff_str="17:00"):
def get_counting_date(dt=None, cutoff_str=CUTOFF_TIME):
"""Replicate the service logic for determining the counting date."""
if dt is None:
dt = datetime.now()
cutoff = datetime.strptime(cutoff_str, "%H:%M").time()
if dt.time() < cutoff:
return dt.date().isoformat()
return (dt.date() + timedelta(days=1)).isoformat()
# ------------------------------------------------------------------ #
# Routes
# ------------------------------------------------------------------ #
@app.route("/")
def index():
"""Main dashboard page."""
return render_template("dashboard.html")
@app.route("/api/current-batch")
def api_current_batch():
"""Get real-time data from the current active batch file."""
try:
with open(CURRENT_BATCH_PATH, "r") as f:
data = json.load(f)
return jsonify(
{
"success": True,
"counting_date": data.get("counting_date"),
"batch_number": data.get("batch_number"),
"count": data.get("count", 0),
"start_time": data.get("start_time"),
"last_detection_time": data.get("last_detection_time"),
}
)
except FileNotFoundError:
return jsonify(
{
"success": False,
"error": "No active batch",
"count": 0,
"batch_number": None,
"counting_date": None,
}
), 404
except Exception as e:
return jsonify(
{
"success": False,
"error": str(e),
"count": 0,
"batch_number": None,
"counting_date": None,
}
), 500
@app.route("/api/summary")
def api_summary():
"""Get summary statistics for the dashboard cards."""
conn = get_db()
cur = conn.cursor()
# Today's counting date (based on cutoff)
today = get_counting_date()
# Today's stats
cur.execute(
"""
SELECT COALESCE(total_count, 0) as total_count,
COALESCE(total_batches, 0) as total_batches
FROM daily_summaries
WHERE counting_date = ?
""",
(today,),
)
today_row = cur.fetchone()
# Yesterday's stats
yesterday = (
datetime.strptime(today, "%Y-%m-%d").date() - timedelta(days=1)
).isoformat()
cur.execute(
"""
SELECT COALESCE(total_count, 0) as total_count,
COALESCE(total_batches, 0) as total_batches
FROM daily_summaries
WHERE counting_date = ?
""",
(yesterday,),
)
yesterday_row = cur.fetchone()
# All-time totals
cur.execute("""
SELECT COALESCE(SUM(total_count), 0) as grand_total,
COALESCE(SUM(total_batches), 0) as grand_batches,
COUNT(DISTINCT counting_date) as total_days
FROM daily_summaries
""")
all_time = cur.fetchone()
# Average per day
cur.execute("""
SELECT ROUND(AVG(total_count), 1) as avg_per_day
FROM daily_summaries
""")
avg = cur.fetchone()
# Best day
cur.execute("""
SELECT counting_date, total_count
FROM daily_summaries
ORDER BY total_count DESC
LIMIT 1
""")
best = cur.fetchone()
conn.close()
return jsonify(
{
"today": {
"date": today,
"total_count": today_row["total_count"] if today_row else 0,
"total_batches": today_row["total_batches"] if today_row else 0,
},
"yesterday": {
"date": yesterday,
"total_count": yesterday_row["total_count"] if yesterday_row else 0,
"total_batches": yesterday_row["total_batches"] if yesterday_row else 0,
},
"all_time": {
"grand_total": all_time["grand_total"],
"grand_batches": all_time["grand_batches"],
"total_days": all_time["total_days"],
},
"average_per_day": avg["avg_per_day"] or 0,
"best_day": {
"date": best["counting_date"] if best else None,
"count": best["total_count"] if best else 0,
},
}
)
@app.route("/api/daily-data")
def api_daily_data():
"""Get daily data for charts and table."""
days = request.args.get("days", 30, type=int)
date_from = (datetime.now() - timedelta(days=days)).date().isoformat()
conn = get_db()
cur = conn.cursor()
# Daily summaries for chart
cur.execute(
"""
SELECT counting_date, total_count, total_batches,
ROUND(CAST(total_count AS FLOAT) / total_batches, 1) as avg_per_batch
FROM daily_summaries
WHERE counting_date >= ?
ORDER BY counting_date ASC
""",
(date_from,),
)
daily_data = []
for row in cur.fetchall():
daily_data.append(
{
"date": row["counting_date"],
"total_count": row["total_count"],
"total_batches": row["total_batches"],
"avg_per_batch": row["avg_per_batch"] or 0,
}
)
conn.close()
return jsonify(daily_data)
@app.route("/api/day-detail/<date>")
def api_day_detail(date):
"""Get detailed batch information for a specific day."""
conn = get_db()
cur = conn.cursor()
# Batches for this day
cur.execute(
"""
SELECT batch_number, count, start_time, end_time,
ROUND(
(julianday(end_time) - julianday(start_time)) * 24 * 60, 1
) as duration_minutes
FROM batches
WHERE counting_date = ?
ORDER BY batch_number ASC
""",
(date,),
)
batches = []
total_duration = 0
for row in cur.fetchall():
duration = row["duration_minutes"] or 0
total_duration += duration
batches.append(
{
"batch_number": row["batch_number"],
"count": row["count"],
"start_time": row["start_time"],
"end_time": row["end_time"],
"duration_minutes": duration,
}
)
# Summary for the day
cur.execute(
"""
SELECT total_count, total_batches
FROM daily_summaries
WHERE counting_date = ?
""",
(date,),
)
summary = cur.fetchone()
conn.close()
return jsonify(
{
"date": date,
"total_count": summary["total_count"] if summary else 0,
"total_batches": summary["total_batches"] if summary else 0,
"total_duration_minutes": round(total_duration, 1),
"avg_duration_minutes": round(total_duration / len(batches), 1)
if batches
else 0,
"batches": batches,
}
)
@app.route("/api/recent-batches")
def api_recent_batches():
"""Get the most recent batches across all days."""
limit = request.args.get("limit", 10, type=int)
conn = get_db()
cur = conn.cursor()
cur.execute(
"""
SELECT counting_date, batch_number, count, start_time, end_time,
ROUND(
(julianday(end_time) - julianday(start_time)) * 24 * 60, 1
) as duration_minutes
FROM batches
ORDER BY end_time DESC
LIMIT ?
""",
(limit,),
)
batches = []
for row in cur.fetchall():
batches.append(
{
"date": row["counting_date"],
"batch_number": row["batch_number"],
"count": row["count"],
"start_time": row["start_time"],
"end_time": row["end_time"],
"duration_minutes": row["duration_minutes"] or 0,
}
)
conn.close()
return jsonify(batches)
@app.route("/api/available-dates")
def api_available_dates():
"""Get list of all available counting dates."""
conn = get_db()
cur = conn.cursor()
cur.execute("""
SELECT counting_date, total_count, total_batches
FROM daily_summaries
ORDER BY counting_date DESC
""")
dates = []
for row in cur.fetchall():
dates.append(
{
"date": row["counting_date"],
"total_count": row["total_count"],
"total_batches": row["total_batches"],
}
)
conn.close()
return jsonify(dates)
# ------------------------------------------------------------------ #
# CSV Export Routes
# ------------------------------------------------------------------ #
@app.route("/api/export-daily-csv")
def export_daily_csv():
"""Export daily summary records as CSV."""
days = request.args.get("days", 30, type=int)
date_from = (datetime.now() - timedelta(days=days)).date().isoformat()
conn = get_db()
cur = conn.cursor()
cur.execute(
"""
SELECT counting_date, total_count, total_batches,
ROUND(CAST(total_count AS FLOAT) / NULLIF(total_batches, 0), 1) as avg_per_batch
FROM daily_summaries
WHERE counting_date >= ?
ORDER BY counting_date ASC
""",
(date_from,),
)
output = StringIO()
writer = csv.writer(output)
writer.writerow(["Date", "Total Count", "Total Batches", "Avg per Batch"])
for row in cur.fetchall():
writer.writerow(
[
row["counting_date"],
row["total_count"],
row["total_batches"],
row["avg_per_batch"] or 0,
]
)
conn.close()
csv_data = output.getvalue()
output.close()
filename = f"daily_records_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
return Response(
csv_data,
mimetype="text/csv",
headers={
"Content-Disposition": f"attachment; filename={filename}",
"Content-Type": "text/csv; charset=utf-8",
},
)
@app.route("/api/export-day-csv/<date>")
def export_day_csv(date):
"""Export batch details for a specific day as CSV."""
conn = get_db()
cur = conn.cursor()
# Batches for this day
cur.execute(
"""
SELECT batch_number, count, start_time, end_time,
ROUND(
(julianday(end_time) - julianday(start_time)) * 24 * 60, 1
) as duration_minutes
FROM batches
WHERE counting_date = ?
ORDER BY batch_number ASC
""",
(date,),
)
output = StringIO()
writer = csv.writer(output)
writer.writerow(
["Batch Number", "Count", "Start Time", "End Time", "Duration (min)"]
)
for row in cur.fetchall():
writer.writerow(
[
row["batch_number"],
row["count"],
row["start_time"],
row["end_time"],
row["duration_minutes"] or 0,
]
)
conn.close()
csv_data = output.getvalue()
output.close()
filename = f"day_detail_{date}.csv"
return Response(
csv_data,
mimetype="text/csv",
headers={
"Content-Disposition": f"attachment; filename={filename}",
"Content-Type": "text/csv; charset=utf-8",
},
)
# ------------------------------------------------------------------ #
# Run
# ------------------------------------------------------------------ #
if __name__ == "__main__":
# Suppress Flask's default request log spam
WSGIRequestHandler.protocol_version = "HTTP/1.1"
port = int(os.getenv("DASHBOARD_PORT", 80))
host = os.getenv("DASHBOARD_HOST", "0.0.0.0")
debug = os.getenv("FLASK_DEBUG", "false").lower() == "true"
print(f"🚀 Dashboard running at http://{host}:{port}")
app.run(host=host, port=port, debug=debug)