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 %} -
+
{% for effect in effects %} {% set meta = effect.meta or effect %} + class="effect-card bg-gray-800 border border-gray-700 rounded-lg p-4 hover:border-gray-600 transition-colors"> + + {% if has_more %} +
+ Loading more... +
+ {% endif %} + {% else %}

No effects uploaded yet.