""" 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.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.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: await recipe_service.delete_recipe(recipe["recipe_id"], actor_id) deleted["recipes"] += 1 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 try: cache_manager = get_cache_manager() effects_dir = Path(cache_manager.cache_dir) / "_effects" if effects_dir.exists(): import shutil for effect_dir in effects_dir.iterdir(): if effect_dir.is_dir(): try: shutil.rmtree(effect_dir) deleted["effects"] += 1 except Exception as e: errors.append(f"Effect {effect_dir.name}: {e}") except Exception as e: errors.append(f"Failed to delete effects: {e}") # Delete all media/cache items for user try: items = await database.get_user_items(actor_id, limit=10000) for item in items: try: cid = item.get("cid") if cid: await database.delete_cache_item(cid) deleted["media"] += 1 except Exception as e: errors.append(f"Media {item.get('cid', 'unknown')}: {e}") except Exception as e: errors.append(f"Failed to list 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" )