Fix /recipes redirect loop by merging duplicate routes
Removed duplicate /recipes route that was causing infinite redirect. Now single route handles both HTML and JSON responses directly. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
139
server.py
139
server.py
@@ -1055,19 +1055,76 @@ async def list_recipes_api(request: Request, page: int = 1, limit: int = 20):
|
|||||||
current_user = get_user_from_cookie(request)
|
current_user = get_user_from_cookie(request)
|
||||||
|
|
||||||
all_recipes = list_all_recipes()
|
all_recipes = list_all_recipes()
|
||||||
total = len(all_recipes)
|
|
||||||
|
|
||||||
# Pagination
|
if wants_html(request):
|
||||||
|
# HTML response
|
||||||
|
if not current_user:
|
||||||
|
return HTMLResponse(render_page(
|
||||||
|
"Recipes",
|
||||||
|
'<p class="text-gray-400 py-8 text-center"><a href="/login" class="text-blue-400 hover:text-blue-300">Login</a> to see recipes.</p>',
|
||||||
|
None,
|
||||||
|
active_tab="recipes"
|
||||||
|
))
|
||||||
|
|
||||||
|
# Filter to user's recipes
|
||||||
|
actor_id = f"@{current_user}@{L2_DOMAIN}"
|
||||||
|
user_recipes = [c for c in all_recipes if c.uploader in (current_user, actor_id)]
|
||||||
|
total = len(user_recipes)
|
||||||
|
|
||||||
|
if not user_recipes:
|
||||||
|
content = '''
|
||||||
|
<h2 class="text-xl font-semibold text-white mb-6">Recipes (0)</h2>
|
||||||
|
<p class="text-gray-400 py-8 text-center">No recipes yet. Upload a recipe YAML file to get started.</p>
|
||||||
|
'''
|
||||||
|
return HTMLResponse(render_page("Recipes", content, current_user, active_tab="recipes"))
|
||||||
|
|
||||||
|
html_parts = []
|
||||||
|
for recipe in user_recipes:
|
||||||
|
var_count = len(recipe.variable_inputs)
|
||||||
|
fixed_count = len(recipe.fixed_inputs)
|
||||||
|
input_info = []
|
||||||
|
if var_count:
|
||||||
|
input_info.append(f"{var_count} variable")
|
||||||
|
if fixed_count:
|
||||||
|
input_info.append(f"{fixed_count} fixed")
|
||||||
|
inputs_str = ", ".join(input_info) if input_info else "no inputs"
|
||||||
|
|
||||||
|
html_parts.append(f'''
|
||||||
|
<a href="/recipe/{recipe.recipe_id}" class="block">
|
||||||
|
<div class="bg-dark-700 rounded-lg p-4 hover:bg-dark-600 transition-colors">
|
||||||
|
<div class="flex flex-wrap items-center justify-between gap-3 mb-3">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<span class="px-3 py-1 bg-purple-600 text-white text-sm font-medium rounded-full">{recipe.name}</span>
|
||||||
|
<span class="text-gray-400 text-xs">v{recipe.version}</span>
|
||||||
|
</div>
|
||||||
|
<span class="text-xs text-gray-400">{inputs_str}</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-gray-400 mb-2">
|
||||||
|
{recipe.description or "No description"}
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-gray-500 font-mono truncate">
|
||||||
|
{recipe.recipe_id[:24]}...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
''')
|
||||||
|
|
||||||
|
content = f'''
|
||||||
|
<h2 class="text-xl font-semibold text-white mb-6">Recipes ({total})</h2>
|
||||||
|
<div class="space-y-4">
|
||||||
|
{''.join(html_parts)}
|
||||||
|
</div>
|
||||||
|
'''
|
||||||
|
|
||||||
|
return HTMLResponse(render_page("Recipes", content, current_user, active_tab="recipes"))
|
||||||
|
|
||||||
|
# JSON response for APIs
|
||||||
|
total = len(all_recipes)
|
||||||
start = (page - 1) * limit
|
start = (page - 1) * limit
|
||||||
end = start + limit
|
end = start + limit
|
||||||
recipes_page = all_recipes[start:end]
|
recipes_page = all_recipes[start:end]
|
||||||
has_more = end < total
|
has_more = end < total
|
||||||
|
|
||||||
if wants_html(request):
|
|
||||||
# HTML response - redirect to /recipes page with proper UI
|
|
||||||
return RedirectResponse(f"/recipes?page={page}")
|
|
||||||
|
|
||||||
# JSON response for APIs
|
|
||||||
return {
|
return {
|
||||||
"recipes": [c.model_dump() for c in recipes_page],
|
"recipes": [c.model_dump() for c in recipes_page],
|
||||||
"pagination": {
|
"pagination": {
|
||||||
@@ -1222,74 +1279,6 @@ def build_dag_from_recipe(yaml_config: dict, user_inputs: dict[str, str], recipe
|
|||||||
|
|
||||||
# ============ Recipe UI Pages ============
|
# ============ Recipe UI Pages ============
|
||||||
|
|
||||||
@app.get("/recipes", response_class=HTMLResponse)
|
|
||||||
async def recipes_page(request: Request, page: int = 1):
|
|
||||||
"""Recipes list page (HTML)."""
|
|
||||||
current_user = get_user_from_cookie(request)
|
|
||||||
|
|
||||||
if not current_user:
|
|
||||||
return HTMLResponse(render_page(
|
|
||||||
"Recipes",
|
|
||||||
'<p class="text-gray-400 py-8 text-center"><a href="/login" class="text-blue-400 hover:text-blue-300">Login</a> to see recipes.</p>',
|
|
||||||
None,
|
|
||||||
active_tab="recipes"
|
|
||||||
))
|
|
||||||
|
|
||||||
all_recipes = list_all_recipes()
|
|
||||||
|
|
||||||
# Filter to user's configs
|
|
||||||
actor_id = f"@{current_user}@{L2_DOMAIN}"
|
|
||||||
user_recipes = [c for c in all_recipes if c.uploader in (current_user, actor_id)]
|
|
||||||
total = len(user_recipes)
|
|
||||||
|
|
||||||
if not user_recipes:
|
|
||||||
content = '''
|
|
||||||
<h2 class="text-xl font-semibold text-white mb-6">Recipes (0)</h2>
|
|
||||||
<p class="text-gray-400 py-8 text-center">No recipes yet. Upload a recipe YAML file to get started.</p>
|
|
||||||
'''
|
|
||||||
return HTMLResponse(render_page("Recipes", content, current_user, active_tab="recipes"))
|
|
||||||
|
|
||||||
html_parts = []
|
|
||||||
for recipe in user_recipes:
|
|
||||||
var_count = len(recipe.variable_inputs)
|
|
||||||
fixed_count = len(recipe.fixed_inputs)
|
|
||||||
input_info = []
|
|
||||||
if var_count:
|
|
||||||
input_info.append(f"{var_count} variable")
|
|
||||||
if fixed_count:
|
|
||||||
input_info.append(f"{fixed_count} fixed")
|
|
||||||
inputs_str = ", ".join(input_info) if input_info else "no inputs"
|
|
||||||
|
|
||||||
html_parts.append(f'''
|
|
||||||
<a href="/recipe/{recipe.recipe_id}" class="block">
|
|
||||||
<div class="bg-dark-700 rounded-lg p-4 hover:bg-dark-600 transition-colors">
|
|
||||||
<div class="flex flex-wrap items-center justify-between gap-3 mb-3">
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<span class="px-3 py-1 bg-purple-600 text-white text-sm font-medium rounded-full">{recipe.name}</span>
|
|
||||||
<span class="text-gray-400 text-xs">v{recipe.version}</span>
|
|
||||||
</div>
|
|
||||||
<span class="text-xs text-gray-400">{inputs_str}</span>
|
|
||||||
</div>
|
|
||||||
<div class="text-sm text-gray-400 mb-2">
|
|
||||||
{recipe.description or "No description"}
|
|
||||||
</div>
|
|
||||||
<div class="text-xs text-gray-500 font-mono truncate">
|
|
||||||
{recipe.recipe_id[:24]}...
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
''')
|
|
||||||
|
|
||||||
content = f'''
|
|
||||||
<h2 class="text-xl font-semibold text-white mb-6">Recipes ({total})</h2>
|
|
||||||
<div class="space-y-4">
|
|
||||||
{''.join(html_parts)}
|
|
||||||
</div>
|
|
||||||
'''
|
|
||||||
|
|
||||||
return HTMLResponse(render_page("Recipes", content, current_user, active_tab="recipes"))
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/recipe/{recipe_id}", response_class=HTMLResponse)
|
@app.get("/recipe/{recipe_id}", response_class=HTMLResponse)
|
||||||
async def recipe_detail_page(recipe_id: str, request: Request):
|
async def recipe_detail_page(recipe_id: str, request: Request):
|
||||||
"""Recipe detail page with run form."""
|
"""Recipe detail page with run form."""
|
||||||
|
|||||||
Reference in New Issue
Block a user