From 7d24ba4dd7fa47e62551b78fb1ddf2793b49ac19 Mon Sep 17 00:00:00 2001
From: gilesb
Date: Mon, 12 Jan 2026 14:22:09 +0000
Subject: [PATCH] Add pagination and API improvements
- Add pagination to effects list with infinite scroll
- Refactor home stats into reusable get_user_stats function
- Add /api/stats endpoint for CLI/API clients
- Add has_more flag to recipes listing
- Add JSON API support to storage type page
Co-Authored-By: Claude Opus 4.5
---
app/routers/effects.py | 16 +++++-
app/routers/home.py | 99 +++++++++++++++++++--------------
app/routers/recipes.py | 6 +-
app/routers/storage.py | 9 +++
app/templates/effects/list.html | 15 ++++-
5 files changed, 98 insertions(+), 47 deletions(-)
diff --git a/app/routers/effects.py b/app/routers/effects.py
index 3316ee3..fd85a8a 100644
--- a/app/routers/effects.py
+++ b/app/routers/effects.py
@@ -310,9 +310,11 @@ async def get_effect_source(
@router.get("")
async def list_effects(
request: Request,
+ offset: int = 0,
+ limit: int = 20,
ctx: UserContext = Depends(require_auth),
):
- """List all uploaded effects."""
+ """List uploaded effects with pagination."""
effects_dir = get_effects_dir()
effects = []
@@ -341,18 +343,26 @@ async def list_effects(
# Sort by upload time (newest first)
effects.sort(key=lambda e: e.get("uploaded_at", ""), reverse=True)
+ # Apply pagination
+ total = len(effects)
+ paginated_effects = effects[offset:offset + limit]
+ has_more = offset + limit < total
+
if wants_json(request):
- return {"effects": effects}
+ return {"effects": paginated_effects, "offset": offset, "limit": limit, "has_more": has_more}
from ..dependencies import get_nav_counts
nav_counts = await get_nav_counts(ctx.actor_id)
templates = get_templates(request)
return render(templates, "effects/list.html", request,
- effects=effects,
+ effects=paginated_effects,
user=ctx,
nav_counts=nav_counts,
active_tab="effects",
+ offset=offset,
+ limit=limit,
+ has_more=has_more,
)
diff --git a/app/routers/home.py b/app/routers/home.py
index 0780066..50ae2b2 100644
--- a/app/routers/home.py
+++ b/app/routers/home.py
@@ -16,12 +16,68 @@ from ..dependencies import get_templates, get_current_user
router = APIRouter()
+async def get_user_stats(actor_id: str) -> dict:
+ """Get stats for a user."""
+ import database
+ from ..services.recipe_service import RecipeService
+ from ..services.run_service import RunService
+ from ..dependencies import get_redis_client, get_cache_manager
+
+ stats = {}
+
+ try:
+ stats["media"] = await database.count_user_items(actor_id)
+ except Exception:
+ stats["media"] = 0
+
+ try:
+ recipe_service = RecipeService(get_redis_client(), get_cache_manager())
+ recipes = await recipe_service.list_recipes(actor_id)
+ stats["recipes"] = len(recipes)
+ except Exception:
+ stats["recipes"] = 0
+
+ try:
+ run_service = RunService(database, get_redis_client(), get_cache_manager())
+ runs = await run_service.list_runs(actor_id)
+ stats["runs"] = len(runs)
+ except Exception:
+ stats["runs"] = 0
+
+ try:
+ storage_providers = await database.get_user_storage_providers(actor_id)
+ stats["storage"] = len(storage_providers) if storage_providers else 0
+ except Exception:
+ stats["storage"] = 0
+
+ try:
+ effects_dir = Path(get_cache_manager().cache_dir) / "_effects"
+ if effects_dir.exists():
+ stats["effects"] = len([d for d in effects_dir.iterdir() if d.is_dir()])
+ else:
+ stats["effects"] = 0
+ except Exception:
+ stats["effects"] = 0
+
+ return stats
+
+
+@router.get("/api/stats")
+async def api_stats(request: Request):
+ """Get user stats as JSON for CLI and API clients."""
+ user = await get_current_user(request)
+ if not user:
+ raise HTTPException(401, "Authentication required")
+
+ stats = await get_user_stats(user.actor_id)
+ return stats
+
+
@router.get("/")
async def home(request: Request):
"""
Home page - show README and stats.
"""
- import database
user = await get_current_user(request)
# Load README
@@ -36,46 +92,7 @@ async def home(request: Request):
# Get stats for current user
stats = {}
if user:
- try:
- stats["media"] = await database.count_user_items(user.actor_id)
- except Exception:
- pass
- try:
- from ..services.recipe_service import RecipeService
- from ..dependencies import get_redis_client, get_cache_manager
- import logging
- logger = logging.getLogger(__name__)
- recipe_service = RecipeService(get_redis_client(), get_cache_manager())
- recipes = await recipe_service.list_recipes(user.actor_id)
- stats["recipes"] = len(recipes)
- logger.info(f"Home page: found {len(recipes)} recipes for {user.actor_id}")
- except Exception as e:
- import logging
- logging.getLogger(__name__).error(f"Failed to get recipe count: {e}")
- try:
- from ..services.run_service import RunService
- from ..dependencies import get_redis_client, get_cache_manager
- run_service = RunService(database, get_redis_client(), get_cache_manager())
- runs = await run_service.list_runs(user.actor_id)
- stats["runs"] = len(runs)
- except Exception:
- pass
- try:
- storage_providers = await database.get_user_storage_providers(user.actor_id)
- stats["storage"] = len(storage_providers) if storage_providers else 0
- except Exception:
- pass
- try:
- # Effects are stored in _effects/ directory, not in cache
- from pathlib import Path
- from ..dependencies import get_cache_manager
- effects_dir = Path(get_cache_manager().cache_dir) / "_effects"
- if effects_dir.exists():
- stats["effects"] = len([d for d in effects_dir.iterdir() if d.is_dir()])
- else:
- stats["effects"] = 0
- except Exception:
- pass
+ stats = await get_user_stats(user.actor_id)
templates = get_templates(request)
return render(templates, "home.html", request,
diff --git a/app/routers/recipes.py b/app/routers/recipes.py
index ff4db6e..41f7ee3 100644
--- a/app/routers/recipes.py
+++ b/app/routers/recipes.py
@@ -377,9 +377,10 @@ async def list_recipes(
return RedirectResponse(url="/auth", status_code=302)
recipes = await recipe_service.list_recipes(ctx.actor_id, offset=offset, limit=limit)
+ has_more = len(recipes) >= limit
if wants_json(request):
- return {"recipes": recipes, "offset": offset, "limit": limit}
+ return {"recipes": recipes, "offset": offset, "limit": limit, "has_more": has_more}
from ..dependencies import get_nav_counts
nav_counts = await get_nav_counts(ctx.actor_id)
@@ -390,6 +391,9 @@ async def list_recipes(
user=ctx,
nav_counts=nav_counts,
active_tab="recipes",
+ offset=offset,
+ limit=limit,
+ has_more=has_more,
)
diff --git a/app/routers/storage.py b/app/routers/storage.py
index 074d440..92ee4f0 100644
--- a/app/routers/storage.py
+++ b/app/routers/storage.py
@@ -271,6 +271,8 @@ async def storage_type_page(
ctx = auth_service.get_user_from_cookie(request)
if not ctx:
+ if wants_json(request):
+ raise HTTPException(401, "Authentication required")
return RedirectResponse(url="/auth", status_code=302)
if provider_type not in STORAGE_PROVIDERS_INFO:
@@ -279,6 +281,13 @@ async def storage_type_page(
storages = await storage_service.list_by_type(ctx.actor_id, provider_type)
provider_info = STORAGE_PROVIDERS_INFO[provider_type]
+ if wants_json(request):
+ return {
+ "provider_type": provider_type,
+ "provider_info": provider_info,
+ "storages": storages,
+ }
+
from ..dependencies import get_nav_counts
nav_counts = await get_nav_counts(ctx.actor_id)
diff --git a/app/templates/effects/list.html b/app/templates/effects/list.html
index 6a76839..74b7696 100644
--- a/app/templates/effects/list.html
+++ b/app/templates/effects/list.html
@@ -18,11 +18,11 @@
{% if effects %}
-