diff --git a/app/main.py b/app/main.py index 3032080..da48409 100644 --- a/app/main.py +++ b/app/main.py @@ -140,10 +140,22 @@ def report(service_id): from_ts, to_ts, from_display, to_display = _parse_report_dates(from_ts, to_ts, preset) status_filter = request.args.get("status") search = request.args.get("search", "").strip() or None + page = max(1, int(request.args.get("page", 1))) + per_page = min(100, max(10, int(request.args.get("per_page", 25)))) stats = models.get_report_stats(service_id, from_ts=from_ts, to_ts=to_ts) - checks = models.get_checks(service_id, limit=100, from_ts=from_ts, to_ts=to_ts, status_filter=status_filter, search=search) + checks_total = models.get_checks_count(service_id, from_ts=from_ts, to_ts=to_ts, status_filter=status_filter, search=search) + checks = models.get_checks( + service_id, + limit=per_page, + offset=(page - 1) * per_page, + from_ts=from_ts, + to_ts=to_ts, + status_filter=status_filter, + search=search, + ) chart_checks = models.get_checks(service_id, limit=200, from_ts=from_ts, to_ts=to_ts) period_label = _format_period_label(from_display, to_display) if (from_ts or to_ts) else None + total_pages = (checks_total + per_page - 1) // per_page if checks_total else 1 return render_template( "report.html", service=dict(svc), @@ -157,6 +169,10 @@ def report(service_id): preset=preset, status_filter=status_filter or "", search=search or "", + page=page, + per_page=per_page, + checks_total=checks_total, + total_pages=total_pages, ) diff --git a/app/models.py b/app/models.py index 967e0e7..9f9dfab 100644 --- a/app/models.py +++ b/app/models.py @@ -137,27 +137,40 @@ def add_check(service_id: int, success: bool, response_time_ms: float | None, er ) -def get_checks(service_id: int, limit: int = 50, from_ts: str = None, to_ts: str = None, status_filter: str = None, search: str = None): - """Get recent checks for a service, optionally filtered by timestamp, status (ok/error), and error search.""" +def _checks_where_args(service_id: int, from_ts: str = None, to_ts: str = None, status_filter: str = None, search: str = None): + """Build WHERE clause and args for checks queries.""" + q = "WHERE service_id = ?" + args = [service_id] + if from_ts: + q += " AND timestamp >= ?" + args.append(from_ts) + if to_ts: + q += " AND timestamp <= ?" + args.append(to_ts) + if status_filter == "error": + q += " AND status = 'ERROR'" + elif status_filter == "ok": + q += " AND status = 'OK'" + if search: + q += " AND (error_message LIKE ? OR status LIKE ?)" + args.extend([f"%{search}%", f"%{search}%"]) + return q, args + + +def get_checks_count(service_id: int, from_ts: str = None, to_ts: str = None, status_filter: str = None, search: str = None) -> int: + """Count checks matching filters (for pagination).""" + where, args = _checks_where_args(service_id, from_ts, to_ts, status_filter, search) with get_db() as conn: - q = "SELECT * FROM checks WHERE service_id = ?" - args = [service_id] - if from_ts: - q += " AND timestamp >= ?" - args.append(from_ts) - if to_ts: - q += " AND timestamp <= ?" - args.append(to_ts) - if status_filter == "error": - q += " AND status = 'ERROR'" - elif status_filter == "ok": - q += " AND status = 'OK'" - if search: - q += " AND (error_message LIKE ? OR status LIKE ?)" - args.extend([f"%{search}%", f"%{search}%"]) - q += " ORDER BY timestamp DESC LIMIT ?" - args.append(limit) - rows = conn.execute(q, args).fetchall() + row = conn.execute(f"SELECT COUNT(*) FROM checks {where}", args).fetchone() + return row[0] + + +def get_checks(service_id: int, limit: int = 50, offset: int = 0, from_ts: str = None, to_ts: str = None, status_filter: str = None, search: str = None): + """Get recent checks for a service, optionally filtered and paginated.""" + where, args = _checks_where_args(service_id, from_ts, to_ts, status_filter, search) + args.extend([limit, offset]) + with get_db() as conn: + rows = conn.execute(f"SELECT * FROM checks {where} ORDER BY timestamp DESC LIMIT ? OFFSET ?", args).fetchall() return [dict(r) for r in rows] diff --git a/static/style.css b/static/style.css index d78a86e..b1f3ab1 100644 --- a/static/style.css +++ b/static/style.css @@ -254,6 +254,55 @@ h2 { text-overflow: ellipsis; } +.pagination { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + gap: 1rem; + margin: 1rem 0; + padding: 0.75rem 0; +} + +.pagination-info { + font-size: 0.875rem; + color: var(--muted); +} + +.pagination-links { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.25rem; +} + +.pagination-btn { + display: inline-block; + padding: 0.35rem 0.6rem; + font-size: 0.875rem; + background: var(--surface); + color: var(--text); + border: 1px solid var(--muted); + border-radius: 4px; + text-decoration: none; +} + +.pagination-btn:hover { + border-color: var(--accent); + color: var(--accent); +} + +.pagination-btn.pagination-current { + background: var(--accent); + border-color: var(--accent); + color: white; +} + +.pagination-ellipsis { + padding: 0 0.25rem; + color: var(--muted); +} + .btn-delete { padding: 0.25rem 0.5rem; font-size: 0.8rem; diff --git a/templates/report.html b/templates/report.html index dee2e44..fdb4681 100644 --- a/templates/report.html +++ b/templates/report.html @@ -21,6 +21,10 @@
Showing: {{ period_label }}
{% endif %}