From 0264425383fbae6f5f66c353e2bcfeb524c09148 Mon Sep 17 00:00:00 2001 From: Ettore <=> Date: Sun, 10 May 2026 00:48:21 +0200 Subject: [PATCH] Add filters in statistics view --- src/core/schemas.py | 7 +++++ src/routers/stats.py | 38 ++++++++++++++++++----- src/static/admin.html | 44 +++++++++++++++++++++++++++ src/static/admin.js | 71 ++++++++++++++++++++++++++++++++++++++----- 4 files changed, 146 insertions(+), 14 deletions(-) diff --git a/src/core/schemas.py b/src/core/schemas.py index 5c72d87..d2b5554 100644 --- a/src/core/schemas.py +++ b/src/core/schemas.py @@ -137,3 +137,10 @@ class AccessLogResponse(BaseModel): user_agent: Optional[str] success: bool error: Optional[str] + + +class StatsPage(BaseModel): + total: int + page: int + page_size: int + items: list[AccessLogResponse] diff --git a/src/routers/stats.py b/src/routers/stats.py index f0b04f3..ae43ea2 100644 --- a/src/routers/stats.py +++ b/src/routers/stats.py @@ -1,21 +1,45 @@ -from fastapi import APIRouter, Depends +from datetime import datetime +from typing import Optional + +from fastapi import APIRouter, Depends, Query from sqlalchemy.orm import Session from core.database import GateAccessLog, get_db from core.dependencies import require_manager -from core.schemas import AccessLogResponse +from core.schemas import StatsPage router = APIRouter(prefix="/api/admin/stats", tags=["admin-stats"]) -@router.get("", response_model=list[AccessLogResponse]) +@router.get("", response_model=StatsPage) async def get_stats( db: Session = Depends(get_db), _: dict = Depends(require_manager), + gate_id: Optional[int] = Query(None), + keypass_code: Optional[str] = Query(None), + success: Optional[bool] = Query(None), + date_from: Optional[datetime] = Query(None), + date_to: Optional[datetime] = Query(None), + page: int = Query(1, ge=1), + page_size: int = Query(50, ge=1, le=200), ): - return ( - db.query(GateAccessLog) - .order_by(GateAccessLog.timestamp.desc()) - .limit(500) + q = db.query(GateAccessLog) + if gate_id is not None: + q = q.filter(GateAccessLog.gate_id == gate_id) + if keypass_code: + q = q.filter(GateAccessLog.keypass_code.ilike(f"%{keypass_code}%")) + if success is not None: + q = q.filter(GateAccessLog.success == success) + if date_from is not None: + q = q.filter(GateAccessLog.timestamp >= date_from) + if date_to is not None: + q = q.filter(GateAccessLog.timestamp <= date_to) + + total = q.count() + items = ( + q.order_by(GateAccessLog.timestamp.desc()) + .offset((page - 1) * page_size) + .limit(page_size) .all() ) + return StatsPage(total=total, page=page, page_size=page_size, items=items) diff --git a/src/static/admin.html b/src/static/admin.html index d79f580..faeaaeb 100644 --- a/src/static/admin.html +++ b/src/static/admin.html @@ -206,6 +206,40 @@

Gate Access Log

+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + +
+
@@ -221,6 +255,16 @@
+ + +
+ +
+ + + +
+
diff --git a/src/static/admin.js b/src/static/admin.js index ebb1718..9c4a6cf 100644 --- a/src/static/admin.js +++ b/src/static/admin.js @@ -366,6 +366,17 @@ document.getElementById("keypass-form").addEventListener("submit", async e => { async function loadGates() { const rows = await api("GET", "/api/admin/gates"); _allGates = rows; // cache for keypass modal + // Populate the stats gate filter dropdown + const filterGate = document.getElementById("filter-gate"); + const prevGateVal = filterGate.value; + filterGate.innerHTML = ''; + for (const g of rows) { + const opt = document.createElement("option"); + opt.value = g.id; + opt.textContent = g.name; + filterGate.appendChild(opt); + } + filterGate.value = prevGateVal; // restore selection if still valid const isAdmin = _tokenPayload().scope === "admin"; const tbody = document.getElementById("gates-body"); tbody.innerHTML = ""; @@ -521,16 +532,47 @@ document.getElementById("credentials-form").addEventListener("submit", async e = }); // ── Statistics ─────────────────────────────────────────────────────────────── +const STATS_PAGE_SIZE = 50; +let _statsPage = 1; +let _statsTotal = 0; + +function _buildStatsParams() { + const params = new URLSearchParams(); + const keypass = document.getElementById("filter-keypass").value.trim(); + if (keypass) params.set("keypass_code", keypass.toUpperCase()); + const gate = document.getElementById("filter-gate").value; + if (gate) params.set("gate_id", gate); + const success = document.getElementById("filter-success").value; + if (success !== "") params.set("success", success); + const from = document.getElementById("filter-from").value; + if (from) params.set("date_from", new Date(from).toISOString()); + const to = document.getElementById("filter-to").value; + if (to) params.set("date_to", new Date(to).toISOString()); + params.set("page", _statsPage); + params.set("page_size", STATS_PAGE_SIZE); + return params; +} + async function loadStats() { try { - const rows = await api("GET", "/api/admin/stats"); + const data = await api("GET", `/api/admin/stats?${_buildStatsParams()}`); + _statsTotal = data.total; + const totalPages = Math.max(1, Math.ceil(_statsTotal / STATS_PAGE_SIZE)); + + document.getElementById("stats-total-label").textContent = + `${_statsTotal} record${_statsTotal !== 1 ? "s" : ""}`; + document.getElementById("stats-page-label").textContent = + `Page ${_statsPage} of ${totalPages}`; + document.getElementById("btn-stats-prev").disabled = _statsPage <= 1; + document.getElementById("btn-stats-next").disabled = _statsPage >= totalPages; + const tbody = document.getElementById("stats-body"); tbody.innerHTML = ""; - if (!rows.length) { - tbody.innerHTML = 'No access logs yet'; + if (!data.items.length) { + tbody.innerHTML = 'No records match the current filters'; return; } - for (const r of rows) { + for (const r of data.items) { const badge = r.success ? 'OK' : `Fail`; @@ -547,9 +589,24 @@ async function loadStats() { } catch (e) { showToast(e.message, true); } } -document.getElementById("btn-refresh-stats").addEventListener("click", loadStats); - -document.getElementById("btn-refresh-stats").addEventListener("click", loadStats); +document.getElementById("btn-refresh-stats").addEventListener("click", () => { _statsPage = 1; loadStats(); }); +document.getElementById("btn-stats-filter").addEventListener("click", () => { _statsPage = 1; loadStats(); }); +document.getElementById("btn-stats-reset").addEventListener("click", () => { + document.getElementById("filter-keypass").value = ""; + document.getElementById("filter-gate").value = ""; + document.getElementById("filter-success").value = ""; + document.getElementById("filter-from").value = ""; + document.getElementById("filter-to").value = ""; + _statsPage = 1; + loadStats(); +}); +document.getElementById("btn-stats-prev").addEventListener("click", () => { + if (_statsPage > 1) { _statsPage--; loadStats(); } +}); +document.getElementById("btn-stats-next").addEventListener("click", () => { + const totalPages = Math.max(1, Math.ceil(_statsTotal / STATS_PAGE_SIZE)); + if (_statsPage < totalPages) { _statsPage++; loadStats(); } +}); // ── Admin users ─────────────────────────────────────────────────────────────── async function loadAdmins() {