The delete_recipe() returns (success, error) tuple but clear-data wasn't checking the result, so failed deletes weren't being reported. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
243 lines
7.4 KiB
Python
243 lines
7.4 KiB
Python
"""
|
|
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:
|
|
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
|
|
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(
|
|
"<html><body><h1>Login not configured</h1>"
|
|
"<p>No L2 server configured for authentication.</p></body></html>",
|
|
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"
|
|
)
|