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

281 lines
8.2 KiB
Python

#!/usr/bin/env python3
"""
Frigate Counter Dashboard
A beautiful, interactive Flask web app for viewing daily batch counting data.
"""
import os
import sqlite3
from datetime import datetime, timedelta
from flask import Flask, render_template, jsonify, request
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')
# ------------------------------------------------------------------ #
# 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="08:00"):
"""Replicate the service logic for determining the counting date."""
if dt is None:
dt = datetime.now()
cutoff = datetime.strptime(cutoff_str, "%H:%M").time()
print(f"DT: {dt.time()}, CO: {cutoff}")
if dt.time() >= cutoff:
print(f"Return Current day: {dt.date()}")
return dt.date().isoformat()
print(f"Return -1 day: {dt.date() - timedelta(days=1)}")
return (dt.date() - timedelta(days=1)).isoformat()
# ------------------------------------------------------------------ #
# Routes
# ------------------------------------------------------------------ #
@app.route('/')
def index():
"""Main dashboard page."""
return render_template('dashboard.html')
@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)
# ------------------------------------------------------------------ #
# 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)