From 95b5bd26eb7efe7765ec88f38afc9c6ee0ed1fbd Mon Sep 17 00:00:00 2001 From: Shawn Date: Fri, 22 May 2026 17:57:24 -0400 Subject: [PATCH] Add pagination (Prev/Next with counter) and X-Total-Count header --- server.py | 12 ++++++++++-- static/index.html | 50 +++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 58 insertions(+), 4 deletions(-) diff --git a/server.py b/server.py index db72ed7..2ddbb1d 100644 --- a/server.py +++ b/server.py @@ -23,7 +23,7 @@ import pytesseract import piexif from PIL import Image as PILImage -from fastapi import FastAPI, HTTPException, Query, Request, UploadFile, File, Form +from fastapi import FastAPI, HTTPException, Query, Request, UploadFile, File, Form, Response from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse, StreamingResponse from fastapi.staticfiles import StaticFiles @@ -970,8 +970,16 @@ def list_assets( params.extend([limit, offset]) rows = conn.execute(sql, params).fetchall() + # Get total count for pagination + count_sql = "SELECT COUNT(*) FROM assets" + if where: + count_sql += f" WHERE {where}" + total = conn.execute(count_sql, params[:len(params)-2]).fetchone()[0] conn.close() - return [row_to_dict(r) for r in rows] + + response = JSONResponse([row_to_dict(r) for r in rows]) + response.headers["X-Total-Count"] = str(total) + return response @app.get("/api/assets/search") diff --git a/static/index.html b/static/index.html index 8b76d19..bc5f5bc 100644 --- a/static/index.html +++ b/static/index.html @@ -242,6 +242,8 @@ .btn-outline { background: transparent; border: 1.5px solid var(--border); color: var(--text); } .btn-danger { background: transparent; border: 1.5px solid var(--red); color: var(--red); } .btn-green { background: var(--green); color: #000; } + .btn-xs { padding: 6px 12px; font-size: 12px; width: auto; line-height: 1; } + .pagination-bar { display: flex; align-items: center; gap: 6px; } /* ═══════════════════════════════════════════════════════════════════════ INPUTS @@ -1070,6 +1072,7 @@
+ @@ -1297,6 +1300,8 @@ // API WRAPPER // ========================================================================= async function api(url, opts = {}) { + const returnMeta = opts._returnMeta; + delete opts._returnMeta; // Auto-attach auth token if available if (AppState.authToken) { opts.headers = opts.headers || {}; @@ -1314,6 +1319,7 @@ const detail = (data && data.detail) || text || `HTTP ${res.status}`; throw new Error(detail); } + if (returnMeta) return { data, headers: res.headers }; return data; } catch (e) { if (e.name === 'TypeError' && e.message === 'Failed to fetch') { @@ -3014,6 +3020,9 @@ // ── Filter state ────────────────────────────────────────────────────── let assetFilters = { category: null, status: null, make: null, disney_park: null, q: '' }; + let assetOffset = 0; + const ASSET_PAGE_SIZE = 100; + let assetTotal = 0; let assetDebounce = null; let cachedAssets = []; @@ -3053,10 +3062,13 @@ if (assetFilters.make) params.set('make', assetFilters.make); if (assetFilters.q) params.set('q', assetFilters.q); if (assetFilters.assigned_to) params.set('assigned_to', assetFilters.assigned_to); - params.set('limit', '100'); + params.set('limit', String(ASSET_PAGE_SIZE)); + params.set('offset', String(assetOffset)); try { - var assets = await api('/api/assets?' + params.toString()); + var result = await api('/api/assets?' + params.toString(), {_returnMeta: true}); + var assets = result.data; + assetTotal = parseInt(result.headers.get('X-Total-Count')) || assets.length; // Client-side filter fallback if server doesn't support assigned_to var filtered = assetFilters.assigned_to ? assets.filter(function(a) { return a.assigned_to === assetFilters.assigned_to; }) @@ -3066,6 +3078,7 @@ : filtered; renderAssetList(filtered); renderFilterPills(assets); + renderPagination(); } catch (e) { document.getElementById('assetList').innerHTML = '
Failed to load assets
'; @@ -3138,14 +3151,47 @@ assetFilters.status = status; assetFilters.make = make; assetFilters.disney_park = disney_park; + assetOffset = 0; loadAssets(); } function clearAssetSearch() { document.getElementById('assetSearch').value = ''; + assetOffset = 0; loadAssets(); } + // ── Pagination ────────────────────────────────────────────────────────── + function renderPagination() { + const el = document.getElementById('pagination'); + if (assetTotal <= ASSET_PAGE_SIZE) { + el.style.display = 'none'; + return; + } + el.style.display = 'flex'; + const current = assetOffset + 1; + const end = Math.min(assetOffset + ASSET_PAGE_SIZE, assetTotal); + el.innerHTML = + '' + + current + '–' + end + ' of ' + assetTotal + '' + + '' + + ''; + } + + function prevPage() { + if (assetOffset > 0) { + assetOffset = Math.max(0, assetOffset - ASSET_PAGE_SIZE); + _loadAssets(); + } + } + + function nextPage() { + if (assetOffset + ASSET_PAGE_SIZE < assetTotal) { + assetOffset += ASSET_PAGE_SIZE; + _loadAssets(); + } + } + // ── View switching ──────────────────────────────────────────────────── function hideAllAssetViews() { ['assetsListView','assetsDetailView','assetsEditView','assetsImportView'].forEach(id => {