Add plate/color/period filters to events list
New filters on the main page: - has_plate: "Plaque lue ✓" / "Non lue" toggle - color: dropdown populated from distinct colors in DB - date: accepts YYYY (year), YYYY-MM (month), or YYYY-MM-DD (day) — all three formats in a single text input Pagination links preserve all active filters. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
a660af2bd7
commit
cd7c9c4d80
+56
-22
@@ -68,7 +68,34 @@ def insert_event(event_id, camera, start_time, snapshot_path, clip_path, plate,
|
|||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
def get_events(limit=100, offset=0, plate_filter=None, camera_filter=None, date_filter=None):
|
def _apply_date_filter(query, params, date_filter):
|
||||||
|
"""Append date range clause for YYYY, YYYY-MM, or YYYY-MM-DD."""
|
||||||
|
from datetime import datetime
|
||||||
|
s = date_filter.strip()
|
||||||
|
try:
|
||||||
|
if len(s) == 4:
|
||||||
|
dt = datetime.strptime(s, "%Y")
|
||||||
|
ts_start = int(dt.timestamp())
|
||||||
|
ts_end = int(datetime(dt.year + 1, 1, 1).timestamp())
|
||||||
|
elif len(s) == 7:
|
||||||
|
dt = datetime.strptime(s, "%Y-%m")
|
||||||
|
ts_start = int(dt.timestamp())
|
||||||
|
nm = dt.month % 12 + 1
|
||||||
|
ny = dt.year + (1 if dt.month == 12 else 0)
|
||||||
|
ts_end = int(datetime(ny, nm, 1).timestamp())
|
||||||
|
else:
|
||||||
|
dt = datetime.strptime(s, "%Y-%m-%d")
|
||||||
|
ts_start = int(dt.timestamp())
|
||||||
|
ts_end = ts_start + 86400
|
||||||
|
query += " AND start_time >= ? AND start_time < ?"
|
||||||
|
params.extend([ts_start, ts_end])
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return query, params
|
||||||
|
|
||||||
|
|
||||||
|
def get_events(limit=100, offset=0, plate_filter=None, camera_filter=None,
|
||||||
|
date_filter=None, has_plate=None, color_filter=None):
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
query = "SELECT * FROM events WHERE 1=1"
|
query = "SELECT * FROM events WHERE 1=1"
|
||||||
params = []
|
params = []
|
||||||
@@ -79,23 +106,23 @@ def get_events(limit=100, offset=0, plate_filter=None, camera_filter=None, date_
|
|||||||
query += " AND camera = ?"
|
query += " AND camera = ?"
|
||||||
params.append(camera_filter)
|
params.append(camera_filter)
|
||||||
if date_filter:
|
if date_filter:
|
||||||
import time
|
query, params = _apply_date_filter(query, params, date_filter)
|
||||||
from datetime import datetime
|
if has_plate == "yes":
|
||||||
try:
|
query += " AND plate IS NOT NULL AND plate != ''"
|
||||||
dt = datetime.strptime(date_filter, "%Y-%m-%d")
|
elif has_plate == "no":
|
||||||
ts_start = int(dt.timestamp())
|
query += " AND (plate IS NULL OR plate = '')"
|
||||||
ts_end = ts_start + 86400
|
if color_filter:
|
||||||
query += " AND start_time >= ? AND start_time < ?"
|
query += " AND color_name = ?"
|
||||||
params.extend([ts_start, ts_end])
|
params.append(color_filter)
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
query += " ORDER BY start_time DESC LIMIT ? OFFSET ?"
|
query += " ORDER BY start_time DESC LIMIT ? OFFSET ?"
|
||||||
params.extend([limit, offset])
|
params.extend([limit, offset])
|
||||||
rows = conn.execute(query, params).fetchall()
|
rows = conn.execute(query, params).fetchall()
|
||||||
conn.close()
|
conn.close()
|
||||||
return [dict(r) for r in rows]
|
return [dict(r) for r in rows]
|
||||||
|
|
||||||
def count_events(plate_filter=None, camera_filter=None, date_filter=None):
|
|
||||||
|
def count_events(plate_filter=None, camera_filter=None, date_filter=None,
|
||||||
|
has_plate=None, color_filter=None):
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
query = "SELECT COUNT(*) FROM events WHERE 1=1"
|
query = "SELECT COUNT(*) FROM events WHERE 1=1"
|
||||||
params = []
|
params = []
|
||||||
@@ -106,16 +133,14 @@ def count_events(plate_filter=None, camera_filter=None, date_filter=None):
|
|||||||
query += " AND camera = ?"
|
query += " AND camera = ?"
|
||||||
params.append(camera_filter)
|
params.append(camera_filter)
|
||||||
if date_filter:
|
if date_filter:
|
||||||
import time
|
query, params = _apply_date_filter(query, params, date_filter)
|
||||||
from datetime import datetime
|
if has_plate == "yes":
|
||||||
try:
|
query += " AND plate IS NOT NULL AND plate != ''"
|
||||||
dt = datetime.strptime(date_filter, "%Y-%m-%d")
|
elif has_plate == "no":
|
||||||
ts_start = int(dt.timestamp())
|
query += " AND (plate IS NULL OR plate = '')"
|
||||||
ts_end = ts_start + 86400
|
if color_filter:
|
||||||
query += " AND start_time >= ? AND start_time < ?"
|
query += " AND color_name = ?"
|
||||||
params.extend([ts_start, ts_end])
|
params.append(color_filter)
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
count = conn.execute(query, params).fetchone()[0]
|
count = conn.execute(query, params).fetchone()[0]
|
||||||
conn.close()
|
conn.close()
|
||||||
return count
|
return count
|
||||||
@@ -148,6 +173,15 @@ def get_cameras():
|
|||||||
return [r[0] for r in rows]
|
return [r[0] for r in rows]
|
||||||
|
|
||||||
|
|
||||||
|
def get_distinct_colors() -> list[str]:
|
||||||
|
conn = get_db()
|
||||||
|
rows = conn.execute(
|
||||||
|
"SELECT DISTINCT color_name FROM events WHERE color_name IS NOT NULL AND color_name != '' ORDER BY color_name"
|
||||||
|
).fetchall()
|
||||||
|
conn.close()
|
||||||
|
return [r[0] for r in rows]
|
||||||
|
|
||||||
|
|
||||||
# ── Whitelist ─────────────────────────────────────────────────────────────────
|
# ── Whitelist ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def get_whitelist() -> list[dict]:
|
def get_whitelist() -> list[dict]:
|
||||||
|
|||||||
+11
-7
@@ -66,17 +66,18 @@ async def index(
|
|||||||
plate: str = Query(""),
|
plate: str = Query(""),
|
||||||
camera: str = Query(""),
|
camera: str = Query(""),
|
||||||
date: str = Query(""),
|
date: str = Query(""),
|
||||||
|
has_plate: str = Query(""),
|
||||||
|
color: str = Query(""),
|
||||||
):
|
):
|
||||||
limit = 20
|
limit = 20
|
||||||
offset = (page - 1) * limit
|
offset = (page - 1) * limit
|
||||||
events = database.get_events(limit=limit, offset=offset,
|
kw = dict(plate_filter=plate or None, camera_filter=camera or None,
|
||||||
plate_filter=plate or None,
|
date_filter=date or None, has_plate=has_plate or None,
|
||||||
camera_filter=camera or None,
|
color_filter=color or None)
|
||||||
date_filter=date or None)
|
events = database.get_events(limit=limit, offset=offset, **kw)
|
||||||
total = database.count_events(plate_filter=plate or None,
|
total = database.count_events(**kw)
|
||||||
camera_filter=camera or None,
|
|
||||||
date_filter=date or None)
|
|
||||||
cameras = database.get_cameras()
|
cameras = database.get_cameras()
|
||||||
|
colors = database.get_distinct_colors()
|
||||||
total_pages = max(1, (total + limit - 1) // limit)
|
total_pages = max(1, (total + limit - 1) // limit)
|
||||||
|
|
||||||
for ev in events:
|
for ev in events:
|
||||||
@@ -89,9 +90,12 @@ async def index(
|
|||||||
"page": page,
|
"page": page,
|
||||||
"total_pages": total_pages,
|
"total_pages": total_pages,
|
||||||
"cameras": cameras,
|
"cameras": cameras,
|
||||||
|
"colors": colors,
|
||||||
"filter_plate": plate,
|
"filter_plate": plate,
|
||||||
"filter_camera": camera,
|
"filter_camera": camera,
|
||||||
"filter_date": date,
|
"filter_date": date,
|
||||||
|
"filter_has_plate": has_plate,
|
||||||
|
"filter_color": color,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -10,8 +10,10 @@
|
|||||||
body { background: #0f172a; color: #e2e8f0; }
|
body { background: #0f172a; color: #e2e8f0; }
|
||||||
.card { background: #1e293b; border: 1px solid #334155; }
|
.card { background: #1e293b; border: 1px solid #334155; }
|
||||||
.plate { font-family: monospace; letter-spacing: 0.12em; }
|
.plate { font-family: monospace; letter-spacing: 0.12em; }
|
||||||
input { background: #0f172a; border: 1px solid #475569; color: #e2e8f0; border-radius: 6px; padding: 6px 10px; }
|
input, select { background: #0f172a; border: 1px solid #475569; color: #e2e8f0; border-radius: 6px; padding: 6px 10px; }
|
||||||
input:focus { outline: none; border-color: #60a5fa; }
|
input:focus, select:focus { outline: none; border-color: #60a5fa; }
|
||||||
|
select option { background: #0f172a; }
|
||||||
|
.filter-label { color: #64748b; font-size: 0.65rem; text-transform: uppercase; letter-spacing: 0.05em; font-weight: 600; display: block; margin-bottom: 3px; }
|
||||||
.btn { background: #2563eb; color: #fff; border-radius: 6px; padding: 7px 16px; font-size: 0.85rem; cursor: pointer; }
|
.btn { background: #2563eb; color: #fff; border-radius: 6px; padding: 7px 16px; font-size: 0.85rem; cursor: pointer; }
|
||||||
.btn:hover { background: #1d4ed8; }
|
.btn:hover { background: #1d4ed8; }
|
||||||
.btn-ghost { background: #1e293b; border: 1px solid #475569; color: #94a3b8; }
|
.btn-ghost { background: #1e293b; border: 1px solid #475569; color: #94a3b8; }
|
||||||
@@ -59,11 +61,36 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Filters -->
|
<!-- Filters -->
|
||||||
<form method="get" class="flex flex-wrap gap-2 mb-5">
|
<form method="get" class="mb-5 p-3 rounded-xl flex flex-wrap gap-x-3 gap-y-2 items-end" style="background:#1e293b;border:1px solid #334155;">
|
||||||
<input type="text" name="plate" placeholder="Plaque…" value="{{ filter_plate }}" class="w-32">
|
<div>
|
||||||
<input type="date" name="date" value="{{ filter_date }}">
|
<span class="filter-label">Plaque</span>
|
||||||
|
<input type="text" name="plate" placeholder="AB-123-CD…" value="{{ filter_plate }}" class="w-28">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="filter-label">Détection</span>
|
||||||
|
<select name="has_plate" class="w-32">
|
||||||
|
<option value="" {% if not filter_has_plate %}selected{% endif %}>Toutes</option>
|
||||||
|
<option value="yes" {% if filter_has_plate == 'yes' %}selected{% endif %}>Plaque lue ✓</option>
|
||||||
|
<option value="no" {% if filter_has_plate == 'no' %}selected{% endif %}>Non lue</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
{% if colors %}
|
||||||
|
<div>
|
||||||
|
<span class="filter-label">Couleur</span>
|
||||||
|
<select name="color" class="w-28">
|
||||||
|
<option value="" {% if not filter_color %}selected{% endif %}>Toutes</option>
|
||||||
|
{% for c in colors %}
|
||||||
|
<option value="{{ c }}" {% if filter_color == c %}selected{% endif %}>{{ c }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div>
|
||||||
|
<span class="filter-label">Période <span class="normal-case font-normal text-slate-600">(2026 / 2026-06 / 2026-06-04)</span></span>
|
||||||
|
<input type="text" name="date" placeholder="Année, mois ou jour…" value="{{ filter_date }}" class="w-40">
|
||||||
|
</div>
|
||||||
<button type="submit" class="btn">Filtrer</button>
|
<button type="submit" class="btn">Filtrer</button>
|
||||||
{% if filter_plate or filter_date %}
|
{% if filter_plate or filter_date or filter_has_plate or filter_color %}
|
||||||
<a href="/" class="btn btn-ghost">✕ Reset</a>
|
<a href="/" class="btn btn-ghost">✕ Reset</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</form>
|
</form>
|
||||||
@@ -112,13 +139,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if total_pages > 1 %}
|
{% if total_pages > 1 %}
|
||||||
|
{% set fq = "&plate=" ~ filter_plate ~ "&date=" ~ filter_date ~ "&has_plate=" ~ filter_has_plate ~ "&color=" ~ filter_color %}
|
||||||
<div class="flex items-center justify-center gap-2 mt-6">
|
<div class="flex items-center justify-center gap-2 mt-6">
|
||||||
{% if page > 1 %}
|
{% if page > 1 %}
|
||||||
<a href="?page={{ page - 1 }}&plate={{ filter_plate }}&date={{ filter_date }}" class="btn btn-ghost text-sm">← Préc.</a>
|
<a href="?page={{ page - 1 }}{{ fq }}" class="btn btn-ghost text-sm">← Préc.</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<span class="text-slate-400 text-sm">Page {{ page }} / {{ total_pages }}</span>
|
<span class="text-slate-400 text-sm">Page {{ page }} / {{ total_pages }}</span>
|
||||||
{% if page < total_pages %}
|
{% if page < total_pages %}
|
||||||
<a href="?page={{ page + 1 }}&plate={{ filter_plate }}&date={{ filter_date }}" class="btn btn-ghost text-sm">Suiv. →</a>
|
<a href="?page={{ page + 1 }}{{ fq }}" class="btn btn-ghost text-sm">Suiv. →</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
Reference in New Issue
Block a user