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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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() {