""" Home and root routes for L1 server. """ from pathlib import Path import markdown from fastapi import APIRouter, Request, Depends, HTTPException from fastapi.responses import HTMLResponse, RedirectResponse, FileResponse from artdag_common import render from artdag_common.middleware import wants_html 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.run_service import RunService from ..dependencies import get_redis_client, get_cache_manager stats = {} try: # Count only actual media types (video, image, audio), not effects/recipes media_count = 0 for media_type in ["video", "image", "audio", "unknown"]: media_count += await database.count_user_items(actor_id, item_type=media_type) stats["media"] = media_count except Exception: stats["media"] = 0 try: # Count user's recipes from database (ownership-based) stats["recipes"] = await database.count_user_items(actor_id, item_type="recipe") 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: # Count user's effects from database (ownership-based) stats["effects"] = await database.count_user_items(actor_id, item_type="effect") 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.delete("/api/clear-data") async def clear_user_data(request: Request): """ Clear all user L1 data except storage configuration. Deletes: runs, recipes, effects, media/cache items. Preserves: storage provider configurations. """ import logging logger = logging.getLogger(__name__) user = await get_current_user(request) if not user: raise HTTPException(401, "Authentication required") import database from ..services.recipe_service import RecipeService from ..services.run_service import RunService from ..dependencies import get_redis_client, get_cache_manager actor_id = user.actor_id username = user.username deleted = { "runs": 0, "recipes": 0, "effects": 0, "media": 0, } errors = [] # Delete all runs try: run_service = RunService(database, get_redis_client(), get_cache_manager()) runs = await run_service.list_runs(actor_id, offset=0, limit=10000) for run in runs: try: await run_service.discard_run(run["run_id"], actor_id, username) deleted["runs"] += 1 except Exception as e: errors.append(f"Run {run['run_id']}: {e}") except Exception as e: errors.append(f"Failed to list runs: {e}") # Delete all recipes try: recipe_service = RecipeService(get_redis_client(), get_cache_manager()) recipes = await recipe_service.list_recipes(actor_id, offset=0, limit=10000) for recipe in recipes: try: success, error = await recipe_service.delete_recipe(recipe["recipe_id"], actor_id) if success: deleted["recipes"] += 1 else: errors.append(f"Recipe {recipe['recipe_id']}: {error}") except Exception as e: errors.append(f"Recipe {recipe['recipe_id']}: {e}") except Exception as e: errors.append(f"Failed to list recipes: {e}") # Delete all effects (uses ownership model) cache_manager = get_cache_manager() try: # Get user's effects from item_types effect_items = await database.get_user_items(actor_id, item_type="effect", limit=10000) for item in effect_items: cid = item.get("cid") if cid: try: # Remove ownership link await database.delete_item_type(cid, actor_id, "effect") await database.delete_friendly_name(actor_id, cid) # Check if orphaned remaining = await database.get_item_types(cid) if not remaining: # Garbage collect effects_dir = Path(cache_manager.cache_dir) / "_effects" / cid if effects_dir.exists(): import shutil shutil.rmtree(effects_dir) import ipfs_client ipfs_client.unpin(cid) deleted["effects"] += 1 except Exception as e: errors.append(f"Effect {cid[:16]}...: {e}") except Exception as e: errors.append(f"Failed to delete effects: {e}") # Delete all media/cache items for user (uses ownership model) try: from ..services.cache_service import CacheService cache_service = CacheService(database, cache_manager) # Get user's media items (video, image, audio) for media_type in ["video", "image", "audio", "unknown"]: items = await database.get_user_items(actor_id, item_type=media_type, limit=10000) for item in items: cid = item.get("cid") if cid: try: success, error = await cache_service.delete_content(cid, actor_id) if success: deleted["media"] += 1 elif error: errors.append(f"Media {cid[:16]}...: {error}") except Exception as e: errors.append(f"Media {cid[:16]}...: {e}") except Exception as e: errors.append(f"Failed to delete media: {e}") logger.info(f"Cleared data for {actor_id}: {deleted}") if errors: logger.warning(f"Errors during clear: {errors[:10]}") # Log first 10 errors return { "message": "User data cleared", "deleted": deleted, "errors": errors[:10] if errors else [], # Return first 10 errors "storage_preserved": True, } @router.get("/") async def home(request: Request): """ Home page - show README and stats. """ user = await get_current_user(request) # Load README readme_html = "" try: readme_path = Path(__file__).parent.parent.parent / "README.md" if readme_path.exists(): readme_html = markdown.markdown(readme_path.read_text(), extensions=['tables', 'fenced_code']) except Exception: pass # Get stats for current user stats = {} if user: stats = await get_user_stats(user.actor_id) templates = get_templates(request) return render(templates, "home.html", request, user=user, readme_html=readme_html, stats=stats, nav_counts=stats, # Reuse stats for nav counts active_tab="home", ) @router.get("/login") async def login_redirect(request: Request): """ Redirect to L2 for login. """ from ..config import settings if settings.l2_server: # Redirect to L2 login with return URL return_url = str(request.url_for("auth_callback")) login_url = f"{settings.l2_server}/login?return_to={return_url}" return RedirectResponse(url=login_url, status_code=302) # No L2 configured - show error return HTMLResponse( "

Login not configured

" "

No L2 server configured for authentication.

", status_code=503 ) # Client tarball path CLIENT_TARBALL = Path(__file__).parent.parent.parent / "artdag-client.tar.gz" @router.get("/download/client") async def download_client(): """Download the Art DAG CLI client.""" if not CLIENT_TARBALL.exists(): raise HTTPException(404, "Client package not found. Run build-client.sh to create it.") return FileResponse( CLIENT_TARBALL, media_type="application/gzip", filename="artdag-client.tar.gz" )