From a234bc505dda07be7c49e79e5f51944f66605946 Mon Sep 17 00:00:00 2001 From: Lee Otts Date: Sun, 17 May 2026 15:54:57 -0500 Subject: [PATCH] Paginate admin gallery (12/page, configurable up to 60) The /gallery page hard-coded a 6-clip view, so most stored clips were unreachable from the admin UI. The gallery and /api/clips/list endpoint now accept ?page= and ?per_page= and return total/total_pages metadata. The template renders a Showing N-M of T counter and Prev/Next controls; polling preserves the current page and jumps back to page 1 only if the current page empties out (e.g. after deleting the last clip on it). --- web/dashboard.py | 51 ++++++++++++----- web/templates/gallery.html | 111 +++++++++++++++++++++++++++++-------- 2 files changed, 126 insertions(+), 36 deletions(-) diff --git a/web/dashboard.py b/web/dashboard.py index 218b39e..99d3f97 100644 --- a/web/dashboard.py +++ b/web/dashboard.py @@ -425,18 +425,19 @@ class Status: return s -def _get_recent_clips(limit=6): - """Get the most recent clips with their captions.""" +def _get_recent_clips(limit=12, offset=0): + """Return a page of recent clips (newest first) plus the total count on disk.""" if not config.CLIPS_DIR.exists(): - return [] + return [], 0 - mp4s = sorted(config.CLIPS_DIR.glob("*.mp4"), key=os.path.getmtime, reverse=True)[:limit] + all_mp4s = sorted(config.CLIPS_DIR.glob("*.mp4"), key=os.path.getmtime, reverse=True) + total = len(all_mp4s) + page_files = all_mp4s[offset:offset + limit] clips = [] - for p in mp4s: + for p in page_files: size_mb = p.stat().st_size / 1_048_576 mtime = datetime.fromtimestamp(p.stat().st_mtime, tz=_local_tz).strftime("%Y-%m-%d %H:%M:%S") - # Load caption if it exists caption_path = p.with_suffix(".txt") caption = "" if caption_path.exists(): @@ -451,7 +452,7 @@ def _get_recent_clips(limit=6): "time": mtime, "caption": caption, }) - return clips + return clips, total def _classify_log_line(line): @@ -556,7 +557,19 @@ def dashboard(): @app.route("/gallery") def gallery_page(): - return render_template("gallery.html", clips=_get_recent_clips()) + page = max(1, request.args.get("page", 1, type=int) or 1) + per_page = max(1, min(60, request.args.get("per_page", 12, type=int) or 12)) + offset = (page - 1) * per_page + clips, total = _get_recent_clips(limit=per_page, offset=offset) + total_pages = max(1, (total + per_page - 1) // per_page) + return render_template( + "gallery.html", + clips=clips, + page=page, + per_page=per_page, + total=total, + total_pages=total_pages, + ) @app.route("/feeders") @@ -1635,12 +1648,22 @@ def serve_training_photo(label, filename): @app.route("/api/clips/list") def api_clips_list(): - """JSON list of recent clips for live updates.""" - clips = _get_recent_clips() - return {"clips": [ - {"name": c["name"], "size": c["size"], "time": c["time"], "caption": c.get("caption", "")} - for c in clips - ]} + """JSON list of recent clips for live updates, with pagination metadata.""" + page = max(1, request.args.get("page", 1, type=int) or 1) + per_page = max(1, min(60, request.args.get("per_page", 12, type=int) or 12)) + offset = (page - 1) * per_page + clips, total = _get_recent_clips(limit=per_page, offset=offset) + total_pages = max(1, (total + per_page - 1) // per_page) + return { + "clips": [ + {"name": c["name"], "size": c["size"], "time": c["time"], "caption": c.get("caption", "")} + for c in clips + ], + "page": page, + "per_page": per_page, + "total": total, + "total_pages": total_pages, + } @app.route("/api/season", methods=["POST"]) diff --git a/web/templates/gallery.html b/web/templates/gallery.html index de006cb..862f6c1 100644 --- a/web/templates/gallery.html +++ b/web/templates/gallery.html @@ -30,12 +30,44 @@ gap: 8px; border-top: 1px solid #1e3625; } + .gallery-meta { + color: #a3b8a6; + font-size: 0.9em; + margin-left: 10px; + } + .gallery-pagination { + margin-top: 20px; + display: flex; + gap: 12px; + align-items: center; + justify-content: center; + flex-wrap: wrap; + } + .gallery-pagination .btn-page { + background: #1e3625; + color: #e8f0ea; + padding: 8px 16px; + font-size: 0.9em; + } + .gallery-pagination .btn-page:hover { background: #2a4933; } + .gallery-pagination .btn-page.disabled { + opacity: 0.4; + pointer-events: none; + cursor: not-allowed; + } + .gallery-pagination .page-indicator { + color: #a3b8a6; + font-size: 0.9em; + } {% endblock %} {% block content %}

Gallery

{% if clips %} + + Showing {{ (page - 1) * per_page + 1 }}–{{ (page - 1) * per_page + clips|length }} of {{ total }} + @@ -58,6 +90,21 @@

Gallery

{% endfor %} + {% if total_pages > 1 %} + + {% endif %} + {% elif total and page > 1 %} +

+ Page {{ page }} is empty. + Go to page 1 + ({{ total }} clip{{ 's' if total != 1 else '' }} total). +

{% else %}

No clips recorded yet. Waiting for hummingbirds...

{% endif %} @@ -75,6 +122,10 @@

Gallery

{% block scripts %}