Fix recipe listing, effects count, and add nav counts to all pages

- Fix list_by_type to return node_id (IPFS CID) instead of local hash
- Fix effects count on home page (count from _effects/ directory)
- Add nav_counts to all page templates (recipes, effects, runs, media, storage)
- Add editable metadata section to cache/media detail page
- Show more metadata on recipe detail page (ID, IPFS CID, step count)
- Update tests for new list_by_type behavior

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
gilesb
2026-01-12 13:30:11 +00:00
parent 9bb1c4278e
commit 98ca2a6c81
11 changed files with 172 additions and 18 deletions

View File

@@ -170,9 +170,14 @@ async def get_nav_counts(actor_id: Optional[str] = None) -> dict:
pass
try:
# Effects are stored in _effects/ directory, not in cache
from pathlib import Path
cache_mgr = get_cache_manager()
effects = cache_mgr.list_by_type('effect')
counts["effects"] = len(effects)
effects_dir = Path(cache_mgr.cache_dir) / "_effects"
if effects_dir.exists():
counts["effects"] = len([d for d in effects_dir.iterdir() if d.is_dir()])
else:
counts["effects"] = 0
except Exception:
pass

View File

@@ -75,10 +75,14 @@ async def get_cached(
if not has_access:
raise HTTPException(403, "Access denied")
from ..dependencies import get_nav_counts
nav_counts = await get_nav_counts(ctx.actor_id)
templates = get_templates(request)
return render(templates, "cache/detail.html", request,
cache=cache_item,
user=ctx,
nav_counts=nav_counts,
active_tab="media",
)
@@ -260,10 +264,14 @@ async def list_media(
if wants_json(request):
return {"items": items, "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, "cache/media_list.html", request,
items=items,
user=ctx,
nav_counts=nav_counts,
offset=offset,
limit=limit,
has_more=has_more,

View File

@@ -248,10 +248,14 @@ async def get_effect(
return meta
# HTML response
from ..dependencies import get_nav_counts
nav_counts = await get_nav_counts(ctx.actor_id)
templates = get_templates(request)
return render(templates, "effects/detail.html", request,
effect=meta,
user=ctx,
nav_counts=nav_counts,
active_tab="effects",
)
@@ -308,10 +312,14 @@ async def list_effects(
if wants_json(request):
return {"effects": effects}
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,
user=ctx,
nav_counts=nav_counts,
active_tab="effects",
)

View File

@@ -66,9 +66,14 @@ async def home(request: Request):
except Exception:
pass
try:
# Effects are stored in _effects/ directory, not in cache
from pathlib import Path
from ..dependencies import get_cache_manager
effects = get_cache_manager().list_by_type('effect')
stats["effects"] = len(effects)
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

View File

@@ -330,10 +330,14 @@ async def list_recipes(
if wants_json(request):
return {"recipes": recipes, "offset": offset, "limit": limit}
from ..dependencies import get_nav_counts
nav_counts = await get_nav_counts(ctx.actor_id)
templates = get_templates(request)
return render(templates, "recipes/list.html", request,
recipes=recipes,
user=ctx,
nav_counts=nav_counts,
active_tab="recipes",
)
@@ -456,11 +460,15 @@ async def get_recipe(
if "sexp" not in recipe:
recipe["sexp"] = "; No S-expression source available"
from ..dependencies import get_nav_counts
nav_counts = await get_nav_counts(ctx.actor_id)
templates = get_templates(request)
return render(templates, "recipes/detail.html", request,
recipe=recipe,
dag_elements=dag_elements,
user=ctx,
nav_counts=nav_counts,
active_tab="recipes",
)

View File

@@ -323,6 +323,10 @@ async def get_run(
if not plan_sexp and plan:
plan_sexp = plan_to_sexp(plan, run.get("recipe_name"))
from ..dependencies import get_nav_counts
user = await get_current_user(request)
nav_counts = await get_nav_counts(user.actor_id if user else None)
templates = get_templates(request)
return render(templates, "runs/detail.html", request,
run=run,
@@ -333,6 +337,7 @@ async def get_run(
output_media_type=output_media_type,
plan_sexp=plan_sexp,
recipe_ipfs_cid=recipe_ipfs_cid,
nav_counts=nav_counts,
active_tab="runs",
)
@@ -418,10 +423,14 @@ async def list_runs(
input_previews.append(preview)
run["input_previews"] = input_previews
from ..dependencies import get_nav_counts
nav_counts = await get_nav_counts(ctx.actor_id)
templates = get_templates(request)
return render(templates, "runs/list.html", request,
runs=runs,
user=ctx,
nav_counts=nav_counts,
offset=offset,
limit=limit,
has_more=has_more,

View File

@@ -66,11 +66,16 @@ async def list_storage(
return {"storages": storages}
# Render HTML template
from ..dependencies import get_nav_counts
nav_counts = await get_nav_counts(ctx.actor_id)
templates = get_templates(request)
return render(templates, "storage/list.html", request,
storages=storages,
user=ctx,
nav_counts=nav_counts,
providers_info=STORAGE_PROVIDERS_INFO,
active_tab="storage",
)
@@ -274,10 +279,15 @@ async def storage_type_page(
storages = await storage_service.list_by_type(ctx.actor_id, provider_type)
provider_info = STORAGE_PROVIDERS_INFO[provider_type]
from ..dependencies import get_nav_counts
nav_counts = await get_nav_counts(ctx.actor_id)
templates = get_templates(request)
return render(templates, "storage/type.html", request,
provider_type=provider_type,
provider_info=provider_info,
storages=storages,
user=ctx,
nav_counts=nav_counts,
active_tab="storage",
)

View File

@@ -39,7 +39,43 @@
{% endif %}
</div>
<!-- Metadata -->
<!-- User Metadata (editable) -->
<div id="metadata-section" class="bg-gray-800 rounded-lg border border-gray-700 p-4 mb-6">
<div class="flex items-center justify-between mb-3">
<h3 class="text-lg font-semibold">Details</h3>
<button hx-get="/cache/{{ cache.cid }}/meta-form"
hx-target="#metadata-section"
hx-swap="innerHTML"
class="text-blue-400 hover:text-blue-300 text-sm">
Edit
</button>
</div>
{% if cache.title or cache.description %}
<div class="space-y-2 mb-4">
{% if cache.title %}
<h4 class="text-white font-medium">{{ cache.title }}</h4>
{% endif %}
{% if cache.description %}
<p class="text-gray-400">{{ cache.description }}</p>
{% endif %}
</div>
{% endif %}
{% if cache.tags %}
<div class="flex flex-wrap gap-2 mb-4">
{% for tag in cache.tags %}
<span class="bg-gray-700 text-gray-300 px-2 py-1 rounded text-sm">{{ tag }}</span>
{% endfor %}
</div>
{% endif %}
{% if cache.source_type or cache.source_note %}
<div class="text-sm text-gray-500">
{% if cache.source_type %}Source: {{ cache.source_type }}{% endif %}
{% if cache.source_note %} - {{ cache.source_note }}{% endif %}
</div>
{% endif %}
</div>
<!-- Technical Metadata -->
<div class="grid grid-cols-2 gap-4 mb-6">
<div class="bg-gray-800 rounded-lg p-4">
<div class="text-gray-500 text-sm">CID</div>

View File

@@ -14,16 +14,42 @@
<!-- Header -->
<div class="flex items-center space-x-4 mb-6">
<a href="/recipes" class="text-gray-400 hover:text-white">&larr; Recipes</a>
<h1 class="text-2xl font-bold">{{ recipe.name }}</h1>
<h1 class="text-2xl font-bold">{{ recipe.name or 'Unnamed Recipe' }}</h1>
{% if recipe.version %}
<span class="text-gray-500">v{{ recipe.version }}</span>
{% endif %}
</div>
{% if recipe.description %}
<p class="text-gray-400 mb-6">{{ recipe.description }}</p>
<p class="text-gray-400 mb-4">{{ recipe.description }}</p>
{% endif %}
<!-- Metadata -->
<div class="bg-gray-800 rounded-lg p-4 border border-gray-700 mb-6">
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
<div>
<span class="text-gray-500">Recipe ID</span>
<p class="text-gray-300 font-mono text-xs truncate" title="{{ recipe.recipe_id }}">{{ recipe.recipe_id[:16] }}...</p>
</div>
{% if recipe.ipfs_cid %}
<div>
<span class="text-gray-500">IPFS CID</span>
<p class="text-gray-300 font-mono text-xs truncate" title="{{ recipe.ipfs_cid }}">{{ recipe.ipfs_cid[:16] }}...</p>
</div>
{% endif %}
<div>
<span class="text-gray-500">Steps</span>
<p class="text-gray-300">{{ recipe.step_count or recipe.steps|length }}</p>
</div>
{% if recipe.author %}
<div>
<span class="text-gray-500">Author</span>
<p class="text-gray-300">{{ recipe.author }}</p>
</div>
{% endif %}
</div>
</div>
<!-- DAG Visualization -->
<div class="bg-gray-800 rounded-lg border border-gray-700 mb-6">
<div class="border-b border-gray-700 px-4 py-2 flex items-center justify-between">